Asis
Asis

Reputation: 733

Model for MVVM in iOS

Here's a User model class. This model will be container for data while registering new user, logging an already registered user and displaying profile.

struct User {

    typealias message = (Bool,String)

    var name: String?
    var username: String
    var password: String
    var image: String?

    func isValidForLogin() -> message {

        let emailMessage = isValidEmail(testStr: username)
        let passwordMessage = isValidPassowrd(testStr: password)

        if emailMessage.0 && passwordMessage.0 {
            return (true,"Valid")
        }

        if !emailMessage.0{
            return (emailMessage.0, emailMessage.1)
        }else{
            return (passwordMessage.0, passwordMessage.1)
        }
    }

    func isValidForRegister() -> message {
        if let name = self.name{
            let nameMessage = isValidName(testStr: name)
            let emailMessage = isValidEmail(testStr: username)
            let passwordMessage = isValidPassowrd(testStr: password)

            if emailMessage.0 && passwordMessage.0 && nameMessage.0{
                return (true,"Valid")
            }

            if !emailMessage.0{
                return (emailMessage.0, emailMessage.1)
            }else if !passwordMessage.0{
                return (passwordMessage.0, passwordMessage.1)
            }else{
                return (nameMessage.0, nameMessage.1)
            }
        }
         return (false, "Name " + Constants.emptyField)
    }

    private func isValidName(testStr: String) -> message{
        if testStr.isEmpty{
            return (false, "Name " + Constants.emptyField )
        }
        return (true, "Valid")
    }

    private func isValidPassowrd(testStr: String) -> (Bool, String) {
        if testStr.isEmpty{
            return (false, "Password " + Constants.emptyField )
        }

        if testStr.count > 6{
            return (true, "Valid")
        }
        return (false, Constants.invalidPassword)
    }

    private func isValidEmail(testStr: String) -> message {

        if testStr.isEmpty{
            return (false, "Email " + Constants.emptyField)
        }

        let emailRegEx = "^(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?(?:(?:(?:[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+(?:\\.[-A-Za-z0-9!#$%&’*+/=?^_'{|}~]+)*)|(?:\"(?:(?:(?:(?: )*(?:(?:[!#-Z^-~]|\\[|\\])|(?:\\\\(?:\\t|[ -~]))))+(?: )*)|(?: )+)\"))(?:@)(?:(?:(?:[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(?:\\.[A-Za-z0-9](?:[-A-Za-z0-9]{0,61}[A-Za-z0-9])?)*)|(?:\\[(?:(?:(?:(?:(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))\\.){3}(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9][0-9])|(?:2[0-4][0-9])|(?:25[0-5]))))|(?:(?:(?: )*[!-Z^-~])*(?: )*)|(?:[Vv][0-9A-Fa-f]+\\.[-A-Za-z0-9._~!$&'()*+,;=:]+))\\])))(?:(?:(?:(?: )*(?:(?:(?:\\t| )*\\r\\n)?(?:\\t| )+))+(?: )*)|(?: )+)?$"
        let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
        let result = emailTest.evaluate(with: testStr)
        if result{
            return (result, "Valid")
        }else{
            return (result, Constants.invalidEmail)
        }
    }
}

I am trying to follow MVVM pattern. So, my ViewModel class for RegisterViewViewModel:

struct RegisterViewModel {

    private let minUserNameLength = 4
    private let minPasswordLength = 6

    var name: String
    var email: String
    var password: String

    private var userModel: User{
        return User(name: name, username: email, password: password, image: "")
    }

    func isValid() -> (Bool, String) {
        return userModel.isValidForRegister()
    }

    func register(){
        ....
    }

}

And in my RegisterViewController :

class RegisterViewController: UIViewController{

    @IBOutlet weak var txtName: UITextField!
    @IBOutlet weak var txtUsername: UITextField!
    @IBOutlet weak var txtPassword: UITextField!


    override func viewDidLoad() {
        super.viewDidLoad()

    }

    @IBAction func btnSignUpPressed(_ sender: UIButton) {
        if let name = txtName.text, let email = txtUsername.text, let password = txtPassword.text{
            let userModel = RegisterViewModel(name: name, email: email, password: password)
            let validate = userModel.isValid()

            if validate.0{
                userModel.register()
            }else{
                //do error handling here
                print(validate.1)
            }
        }
    }
}

Am I going in right direction? Any suggestion will be appreciated.

Upvotes: 0

Views: 434

Answers (4)

Jim lai
Jim lai

Reputation: 1409

class RegisterViewController: UIViewController {
    var user = User() {
         didSet {
             // update UI
         }
    }
}

Most MVVM/RxSwift developers don't understand the notion of "over-engineering", as can be seen from all previous answers. Two of them refer you to a even more complicated design pattern, and one of them built the said pattern from scratch.

You don't need any of the RxSwift nonsense. MVVM isn't about having an object called view model and shoving everything to it.

Build a model so that when it changes, it updates associated view.

Simple, as all things should be.

Below is the pinnacle of over-engineering

protocol ViewModel: ViewModelInput, ViewModelOutput {}

After you define all these, write them down, train colleagues, draw diagrams, and implement them, you would've realized that it's all boilerplate and you should just drop them.

Upvotes: 0

Oleh Kudinov
Oleh Kudinov

Reputation: 2543

To implement MVVM in iOS 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

Upvotes: 0

Denis Kutlubaev
Denis Kutlubaev

Reputation: 16114

I would recommend you to use RxSwift with MVVM. Also you could export validation to a separate ValidationService class. Otherwise you will probably have to copy same validation methods between different models.

enum ValidationResult {
    case ok
    case empty
    case validating
    case failed(message: String)
}

extension ValidationResult {
    var isValid: Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }

    var isEmpty: Bool {
        switch self {
        case .empty:
            return true
        default:
            return false
        }
    }
}

class ValidationService {

    let minPasswordCount = 4

    static let shared = ValidationService()

    func validateName(_ name: String) -> Observable<ValidationResult> {
        if name.isEmpty {
            return .just(.empty)
        }

        if name.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil {
            return .just(.failed(message: "Invalid name"))
        }

        return .just(.ok)
    }
}

Upvotes: 1

dduyduong
dduyduong

Reputation: 122

What you are trying to do is not MVVM pattern.

You are creating a new ViewModel when button is clicked. It is the same as you are creating a business class to handle some business logics.

ViewModel and View are communicating through data binding. If you are familiar with RxSwift, the I suggest to use this library: https://github.com/duyduong/DDMvvm

I wrote this library after using it a lot on private projects. There are examples for you to start and understand how MVVM works. Give it a try!

Upvotes: 0

Related Questions