Usage of MVVM in iOS
Asked Answered
B

3

39

I'm an iOS developer and I'm guilty of having Massive View Controllers in my projects so I've been searching for a better way to structure my projects and came across the MVVM (Model-View-ViewModel) architecture. I've been reading a lot of MVVM with iOS and I have a couple of questions. I'll explain my issues with an example.

I have a view controller called LoginViewController.

LoginViewController.swift

import UIKit

class LoginViewController: UIViewController {

    @IBOutlet private var usernameTextField: UITextField!
    @IBOutlet private var passwordTextField: UITextField!

    private let loginViewModel = LoginViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    @IBAction func loginButtonPressed(sender: UIButton) {
        loginViewModel.login()
    }
}

It doesn't have a Model class. But I did create a view model called LoginViewModel to put the validation logic and network calls.

LoginViewModel.swift

import Foundation

class LoginViewModel {

    var username: String?
    var password: String?

    init(username: String? = nil, password: String? = nil) {
        self.username = username
        self.password = password
    }

    func validate() {
        if username == nil || password == nil {
            // Show the user an alert with the error
        }
    }

    func login() {
        // Call the login() method in ApiHandler
        let api = ApiHandler()
        api.login(username!, password: password!, success: { (data) -> Void in
            // Go to the next view controller
        }) { (error) -> Void in
            // Show the user an alert with the error
        }
    }
}
  1. My first question is simply is my MVVM implementation correct? I have this doubt because for example I put the login button's tap event (loginButtonPressed) in the controller. I didn't create a separate view for the login screen because it has only a couple of textfields and a button. Is it acceptable for the controller to have event methods tied to UI elements?

  2. My next question is also about the login button. When the user taps the button, the username and password values should gte passed into the LoginViewModel for validation and if successful, then to the API call. My question how to pass the values to the view model. Should I add two parameters to the login() method and pass them when I call it from the view controller? Or should I declare properties for them in the view model and set their values from the view controller? Which one is acceptable in MVVM?

  3. Take the validate() method in the view model. The user should be notified if either of them are empty. That means after the checking, the result should be returned to the view controller to take necessary actions (show an alert). Same thing with the login() method. Alert the user if the request fails or go to the next view controller if it succeeds. How do I notify the controller of these events from the view model? Is it possible to use binding mechanisms like KVO in cases like this?

  4. What are the other binding mechanisms when using MVVM for iOS? KVO is one. But I read it's not quite suitable for larger projects because it require a lot of boilerplate code (registering/unregistering observers etc). What are other options? I know ReactiveCocoa is a framework used for this but I'm looking to see if there are any other native ones.

All the materials I came across on MVVM on the Internet provided little to no information on these parts I'm looking to clarify, so I'd really appreciate your responses.

Bocock answered 28/11, 2014 at 5:36 Comment(2)
Is that only me or someone else doesn't like network requests made from a view model too?Chios
@Chios I agree, it's best practice to not make network calls in the view model but in the example provided the ApiHandler class has successfully abstracted away the specifics on how a login is being performed. It's just a best guess at this point that there is indeed a network call taking place. The app could be offline and logging in via a local db. We don't know and neither does the view model (which is how it should be). It would be better if the data type for the api variable was a protocol that was implemented by ApiHandler.Appetency
A
38

waddup dude!

1a- You're headed in the right direction. You put loginButtonPressed in the view controller and that is exactly where it should be. Event handlers for controls should always go into the view controller - so that is correct.

1b - in your view model you have comments stating, "show the user an alert with the error". You don't want to display that error from within the validate function. Instead create an enum that has an associated value (where the value is the error message you want to display to the user). Change your validate method so that it returns that enum. Then within your view controller you can evaluate that return value and from there you will display the alert dialog. Remember you only want to use UIKit related classes only within the view controller - never from the view model. View model should only contain business logic.

enum StatusCodes : Equatable
{
    case PassedValidation
    case FailedValidation(String)

    func getFailedMessage() -> String
    {
        switch self
        {
        case StatusCodes.FailedValidation(let msg):
            return msg

        case StatusCodes.OperationFailed(let msg):
            return msg

        default:
            return ""
        }
    }
}

func ==(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
    switch (lhs, rhs)
    {           
    case (.PassedValidation, .PassedValidation):
        return true

    case (.FailedValidation, .FailedValidation):
        return true

    default:
        return false
    }
}

func !=(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
    return !(lhs == rhs)
}

func validate(username : String, password : String) -> StatusCodes
{
     if username.isEmpty || password.isEmpty
     {
          return StatusCodes.FailedValidation("Username and password are required")
     }

     return StatusCodes.PassedValidation
}

2 - this is a matter of preference and ultimately determined by the requirements for your app. In my app I pass these values in via the login() method i.e. login(username, password).

3 - Create a protocol named LoginEventsDelegate and then have a method within it as such:

func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)

However this method should only be used to notify the view controller of the actual results of attempting to login on the remote server. It should have nothing to do with the validation portion. Your validation routine will be handled as discussed above in #1. Have your view controller implement the LoginEventsDelegate. And create a public property on your view model i.e.

class LoginViewModel {
    var delegate : LoginEventsDelegate?  
}

Then in the completion block for your api call you can notify the view controller via the delegate i.e.

func login() {
        // Call the login() method in ApiHandler
        let api = ApiHandler()

        let successBlock =
        {
           [weak self](data) -> Void in

           if let this = self { 
               this.delegate?.loginViewModel_LoginCallFinished(true, "")
           }
        }

        let errorBlock = 
        {
            [weak self] (error) -> Void in

            if let this = self {
                var errMsg = (error != nil) ? error.description : ""
                this.delegate?.loginViewModel_LoginCallFinished(error == nil, errMsg)
            }
        }

        api.login(username!, password: password!, success: successBlock, error: errorBlock)
    }

and your view controller would look like this:

class loginViewController : LoginEventsDelegate {

    func viewDidLoad() {
        viewModel.delegate = self
    }

    func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) {
         if successful {
             //segue to another view controller here
         } else {
             MsgBox(errMsg) 
         }
    }
}

Some would say you can just pass in a closure to the login method and skip the protocol altogether. There are a few reasons why I think that is a bad idea.

Passing a closure from the UI Layer (UIL) to the Business Logic Layer (BLL) would break Separation of Concerns (SOC). The Login() method resides in BLL so essentially you would be saying "hey BLL execute this UIL logic for me". That's an SOC no no!

BLL should only communicate with the UIL via delegate notifications. That way BLL is essentially saying, "Hey UIL, I'm finished executing my logic and here's some data arguments that you can use to manipulate the UI controls as you need to".

So UIL should never ask BLL to execute UI control logic for him. Should only ask BLL to notify him.

4 - I've seen ReactiveCocoa and heard good things about it but have never used it. So can't speak to it from personal experience. I would see how using simple delegate notification (as described in #3) works for you in your scenario. If it meets the need then great, if you're looking for something a bit more complex then maybe look into ReactiveCocoa.

Btw, this also is technically not an MVVM approach since binding and commands are not being used but that's just "ta-may-toe" | "ta-mah-toe" nitpicking IMHO. SOC principles are all the same regardless of which MV* approach you use.

Appetency answered 28/11, 2014 at 21:18 Comment(10)
Really great answer here! Could you explain a bit more about your answer to 3? the part you say: stick with delegate approach rather than pass in a closure to the login method and skip the protocol ? or maybe give some related link of this issue? Really appreciate!!Lujan
Passing a closure from the UI Layer (UIL) to the Business Logic Layer (BLL) would break Separation of Concerns (SOC). The Login() method resides in BLL so essentially you would be saying "hey BLL execute this UIL logic for me". That's an SOC no no! BLL should only communicate with the UIL via delegate notifications. That way BLL is essentially saying, "Hey UIL, I'm finished executing my logic and here's some data arguments that you can use to manipulate the UI controls as you need to". So UIL should never ask BLL to execute UI control logic for him. Should only ask BLL to notify him.Appetency
But what if your closure contains a single statement like self.refresh()? How it is different from a delegate method which also calls the same self.refresh() method inside? In the end, control is anyway passed into an update method of your viewcontroller independent of a selected communication way.Observe
In a word - maintenance; today it might contain a single statement but apps grow, requirements change, etc, etc. If it's a small single dev app, I wouldn't worry about it but on larger teams where you experience turnover and frequent feature changes - the methodology of notification via delegate should be the standard throughout the app.Appetency
HI @JajaHarris, I'm a bit confused about UIL and BLL communication. In many tutorial, in viewmodel classes, I see var closures declarations like this, var updateLoadingStatus: (()->())?. Does this break SOC? Is it better to avoid it?Conversational
@Conversational What happens when I need to disable some buttons also? Just add another closure property. Then requirements change again now we need hide and show some controls. Add another closure property. New developer comes onboard and he gets new story and just puts his logic inside loading status closure - though it has nothing to do with loading status. Best practice would be to A) move to a binding solution where this becomes a moot point or B) provide an events protocol that ViewController implements. Protocol exposes event based methods i.e. dataRetrievalStarting, dataWasRetrieved.Appetency
@Conversational (cont'd) Now in dataRetrievalStarting, controller can display loading spinner. In dataWasRetrieved controller can hide loading spinner. Now when new dev comes on board and gets new story to show/hide add'l controls when data is being retrieved he knows where that logic should live. He won't stuff it into a updateLoadingStatus closure but instead will just make his own showMyControls() method and call it from dataWasRetrieved. View model has no idea what UI tasks take place when it fires its data events off - its of no concern to him. HTH!Appetency
@Conversational (cont'd) If you take notice of methods in viewController you will see this is how Apple approaches the issue also i.e. viewController has viewDidLoad - VC's way of saying "hey just letting you know the view loaded and you can go ahead and do whatever you need to do. I don't care what you do here not my concern - just my job to let you that view was loaded."Appetency
Thank you very much @JajaHarris! This is all sounding right!! What about this?Conversational
The closure can be defined as var onUpdateLoadingStatus: (()->())?. The BLL doesn't need to know the implementation details of UIL, the UIL will supply the implementation details to it, and BLL only give the definition of the closure. So it wouldn't break SOC.Memorial
W
11

MVVM in iOS means creating an object filled with data that your screen uses, separately from your Model classes. It usually maps all the items in your UI that consume or produce data, like labels, textboxes, datasources or dynamic images. It often does some light validation of input (empty field, is valid email or not, positive number, switch is on or not) with validators. These validators are usually separate classes not inline logic.

Your View layer knows about this VM class and observes changes in it to reflects them and also updates the VM class when the user inputs data. All properties in the VM are tied to items in the UI. So for example a user goes to a user registration screen this screen gets a VM that has none of it's properties filled except the status property that has an Incomplete status. The View knows that only a Complete form can be submitted so it sets the Submit button inactive now.

Then the user starts filling in it's details and makes a mistake in the e-mail address format. The Validator for that field in the VM now sets an error state and the View sets the error state (red border for example) and error message that's in the VM validator in the UI.

Finally, when all the required fields inside the VM get the status Complete the VM is Complete, the View observes that and now sets the Submit button to active so the user can submit it. The Submit button action is wired to the VC and the VC makes sure the VM gets linked to the right model(s) and saved. Sometimes Models are used directly as a VM, that might be useful when you have simpler CRUD-like screens.

I've worked with this pattern in WPF and it works really great. It sounds like a lot of trouble setting up all those observers in Views and putting a lot of fields in Model classes as well as ViewModel classes but a good MVVM framework will help you with that. You just need to link UI elements to VM elements of the right type, assign the right Validators and a lot of this plumbing gets done for you without the need for adding all that boilerplate code yourself.

Some advantages of this pattern:

  • It only exposes the data you need
  • Better testability
  • Less boilerplate code to connect UI elements to data

Disadvantages:

  • Now you need to maintain both the M and the VM
  • You still can't completely get around using the VC iOS.
Whorehouse answered 29/11, 2014 at 17:40 Comment(0)
S
4

MVVM architecture in iOS can be easily implemented without using third party dependencies. For data binding, we can use a simple combination of Closure and didSet to avoid third-party dependencies.

public final class Observable<Value> {

    private var closure: ((Value) -> ())?

    public var value: Value {
        didSet { closure?(value) }
    }

    public init(_ value: Value) {
        self.value = value
    }

    public func observe(_ closure: @escaping (Value) -> Void) {
        self.closure = closure
        closure(value)
    }
}

An example of data binding from ViewController:

final class ExampleViewController: UIViewController {

    private func bind(to viewModel: ViewModel) {
        viewModel.items.observe(on: self) { [weak self] items in
            self?.tableViewController?.items = items
            // self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
        }
        // Or in one line:
        viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        bind(to: viewModel)
        viewModel.viewDidLoad()
    }
}

protocol ViewModelInput {
    func viewDidLoad()
}

protocol ViewModelOutput {
    var items: Observable<[ItemViewModel]> { get }
}

protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {  
  let items: Observable<[ItemViewModel]> = Observable([])

  // Implmentation details...
}

Later it can be replaced with SwiftUI and Combine (when a minimum iOS version in of your app is 13)

In this article, there is a more detailed description of MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Sovran answered 7/1, 2020 at 11:30 Comment(1)
Yes Exactly, a simple wrapper (like Observable) is sufficient to MVVM binding. Its the default addon to all my mvvm projects.Arthrospore

© 2022 - 2024 — McMap. All rights reserved.