Подтвердить что ты не робот

Использование MVVM в iOS

Я разработчик iOS, и я виноват в том, что у меня есть контроллеры Massive View, поэтому я искал лучший способ структурирования своих проектов и столкнулся с архитектурой MVVM (Model-View-ViewModel). Я читал много MVVM с iOS, и у меня есть пара вопросов. Я объясню свои проблемы на примере.

У меня есть контроллер вида 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()
    }
}

У него нет класса Model. Но я создал модель представления под названием LoginViewModel, чтобы поместить логику проверки и сетевые вызовы.

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
        }
    }
}
  • Мой первый вопрос - это моя реализация MVVM правильно? У меня есть это сомнение, потому что, например, я ввел в контроллер событие нажатия кнопки входа (loginButtonPressed). Я не создавал отдельное представление для экрана входа, потому что у него есть только несколько текстовых полей и кнопка. Допустимо ли для контроллера использовать методы событий, привязанные к элементам пользовательского интерфейса?

  • Мой следующий вопрос касается кнопки входа в систему. Когда пользователь нажимает кнопку, значения имени пользователя и пароля должны быть переданы в LoginViewModel для проверки и в случае успеха, а затем для вызова API. Мой вопрос, как передать значения модели представления. Должен ли я добавить два параметра в метод login() и передать их, когда я вызываю его из контроллера вида? Или я должен объявлять свойства для них в модели представления и устанавливать их значения из контроллера представления? Какой из них допустим в MVVM?

  • Возьмите метод validate() в модели представления. Пользователь должен быть уведомлен, если любой из них пуст. Это означает, что после проверки результат должен быть возвращен контроллеру представления для принятия необходимых действий (показать предупреждение). То же самое с методом login(). Предупреждать пользователя, если запрос не работает или перейти к следующему контроллеру представления, если он преуспеет. Как я могу уведомить контроллер этих событий из модели представления? Можно ли использовать такие механизмы привязки, как KVO, в таких случаях?

  • Каковы другие механизмы привязки при использовании MVVM для iOS? КВО - одно. Но я читал, что он не подходит для крупных проектов, потому что для этого требуется много шаблонов (регистрация/регистрация регистраторов и т.д.). Какие существуют другие варианты? Я знаю, что ReactiveCocoa - это фреймворк, используемый для этого, но я ищу, есть ли другие родные.

Все материалы, которые я наткнулся на MVVM в Интернете, не дали никакой информации об этих деталях, которые я хочу уточнить, поэтому я действительно ценю ваши ответы.

4b9b3361

Ответ 1

Чувак!

1a- Вы движетесь в правильном направлении. Вы помещаете loginButtonPressed в контроллер представления, и это именно то, что и должно быть. Обработчики событий для элементов управления всегда должны входить в контроллер представления - так что это правильно.

1b - в вашей модели представления есть комментарии о том, что "показать пользователю предупреждение с ошибкой". Вы не хотите отображать эту ошибку из функции проверки. Вместо этого создайте перечисление, которое имеет ассоциированное значение (где значением является сообщение об ошибке, которое вы хотите отобразить пользователю). Измените ваш метод проверки так, чтобы он возвращал это перечисление. Затем в вашем контроллере представления вы можете оценить это возвращаемое значение и оттуда вы увидите диалоговое окно с предупреждением. Помните, что вы хотите использовать только связанные с UIKit классы только в контроллере представления, но не в модели представления. Модель представления должна содержать только бизнес-логику.

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 - это вопрос предпочтений, который в конечном итоге определяется требованиями вашего приложения. В моем приложении я передаю эти значения через метод login(), т.е. login (имя пользователя, пароль).

3 - Создайте протокол с именем LoginEventsDelegate, а затем включите в него метод следующим образом:

func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)

Однако этот метод следует использовать только для уведомления контроллера представления о фактических результатах попыток входа на удаленный сервер. Это не должно иметь ничего общего с частью проверки. Ваша процедура проверки будет обработана, как описано выше в # 1. Пусть ваш контроллер представления реализует LoginEventsDelegate. И создайте публичную собственность в своей модели представления, т.е.

class LoginViewModel {
    var delegate : LoginEventsDelegate?  
}

Затем в блоке завершения для вашего вызова API вы можете уведомить контроллер представления через делегата, т.е.

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)
    }

и ваш контроллер вида будет выглядеть так:

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) 
         }
    }
}

Некоторые скажут, что вы можете просто передать закрытие методу входа и вообще пропустить протокол. Есть несколько причин, почему я считаю это плохой идеей.

Передача закрытия из уровня пользовательского интерфейса (UIL) в уровень бизнес-логики (BLL) нарушит разделение проблем (SOC). Метод Login() находится в BLL, так что, по сути, вы бы сказали: "Эй, BLL, выполни эту логику UIL для меня". Это SOC нет, нет!

BLL должен связываться с UIL только через уведомления делегатов. Таким образом, BLL, по сути, говорит: "Эй, UIL, я закончил выполнять свою логику, и вот некоторые аргументы данных, которые вы можете использовать для манипулирования элементами управления пользовательским интерфейсом по мере необходимости".

Поэтому UIL никогда не должен просить BLL выполнить для него логику управления пользовательским интерфейсом. Следует только попросить BLL уведомить его.

4 - Я видел ReactiveCocoa и слышал о нем много хорошего, но никогда не использовал его. Так что не могу говорить с этим из личного опыта. Я бы посмотрел, как использование простого уведомления делегата (как описано в # 3) работает для вас в вашем сценарии. Если это отвечает потребностям, тогда отлично, если вы ищете что-то более сложное, то, возможно, посмотрите на ReactiveCocoa.

Кстати, технически это не MVVM-подход, так как привязка и команды не используются, а просто "та-май-нолик" | "та-мах-пальчик ног" придирается ИМХО. Принципы SOC одинаковы независимо от того, какой подход MV * вы используете.

Ответ 2

MVVM в iOS означает создание объекта, заполненного данными, которые использует ваш экран, отдельно от классов модели. Он обычно отображает все элементы в пользовательском интерфейсе, которые потребляют или создают данные, такие как метки, текстовые поля, источники данных или динамические изображения. Он часто выполняет некоторую световую проверку ввода (пустое поле, действительный адрес электронной почты или нет, положительное число, переключатель включен или нет) с валидаторами. Эти валидаторы обычно являются отдельными классами, а не встроенной логикой.

Уровень просмотра знает об этом классе VM и наблюдает за изменениями в нем, чтобы отображать их, а также обновляет класс VM, когда пользователь вводит данные. Все свойства виртуальной машины привязаны к элементам пользовательского интерфейса. Так, например, пользователь переходит на экран регистрации пользователя, на этом экране появляется виртуальная машина, у которой нет свойств, заполненных за исключением свойства состояния, которое имеет статус "Незавершенное". "Вид" знает, что может быть отправлена ​​только полная форма, поэтому теперь кнопка "Отправить" неактивна.

Затем пользователь начинает заполнять детали и делает ошибку в формате адреса электронной почты. Валидатор для этого поля в VM теперь устанавливает состояние ошибки, а представление устанавливает состояние ошибки (например, красную границу) и сообщение об ошибке, которое в валидаторе виртуальной машины в пользовательском интерфейсе.

Наконец, когда все необходимые поля внутри виртуальной машины получают статус Завершить VM завершено, View замечает это и теперь устанавливает активную кнопку Submit, чтобы пользователь мог ее отправить. Действие кнопки "Отправить" подключено к VC, и VC гарантирует, что виртуальная машина будет привязана к правильной модели и сохранена. Иногда модели используются непосредственно в качестве виртуальной машины, что может быть полезно, когда у вас более простые экраны, подобные CRUD.

Я работал с этим шаблоном в WPF, и он отлично работает. Похоже, что у многих проблем возникают проблемы с настройкой всех наблюдателей в представлениях и добавлением большого количества полей в классы модели, а также классы ViewModel, но хорошая структура MVVM поможет вам в этом. Вам просто нужно связать элементы пользовательского интерфейса с элементами виртуального типа нужного типа, назначить правильные валидаторы, и многое из этого сантехника будет сделано для вас без необходимости добавления всего этого шаблона.

Некоторые преимущества этого шаблона:

  • Он предоставляет только те данные, которые вам нужны.
  • Лучшая тестируемость
  • Меньше шаблонный код для подключения элементов пользовательского интерфейса к данным

Недостатки:

  • Теперь вам нужно поддерживать как M, так и VM
  • Вы все еще не можете полностью обойтись с помощью VC iOS.