如何使用MVVM/RxSwift基于来自其他单元格的值更新tableview的单元格?

phi*_*phi 4 mvvm ios swift rx-swift rx-cocoa

我是RxSwift的新手,并试图通过创建一个简单的注册表来学习.我想用a来实现它UITableView(作为练习,以及将来会变得更复杂)所以我目前正在使用两种类型的单元:

  • 一个TextInputTableViewCell只有一个UITextField
  • 一个ButtonTableViewCell只有一个UIButton

为了表示每个单元格,我创建了一个看起来像这样的枚举:

enum FormElement {
    case textInput(placeholder: String, text: String?)
    case button(title: String, enabled: Bool)
}
Run Code Online (Sandbox Code Playgroud)

并在a中使用它Variable来提供tableview:

    formElementsVariable = Variable<[FormElement]>([
        .textInput(placeholder: "username", text: nil),
        .textInput(placeholder: "password", text: nil),
        .textInput(placeholder: "password, again", text: nil),
        .button(title: "create account", enabled: false)
        ])
Run Code Online (Sandbox Code Playgroud)

通过这样绑定:

    formElementsVariable.asObservable()
        .bind(to: tableView.rx.items) {
            (tableView: UITableView, index: Int, element: FormElement) in
            let indexPath = IndexPath(row: index, section: 0)
            switch element {
            case .textInput(let placeholder, let defaultText):
                let cell = tableView.dequeueReusableCell(withIdentifier: "TextInputTableViewCell", for: indexPath) as! TextInputTableViewCell
                cell.textField.placeholder = placeholder
                cell.textField.text = defaultText
                return cell
            case .button(let title, let enabled):
                let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonTableViewCell", for: indexPath) as! ButtonTableViewCell
                cell.button.setTitle(title, for: .normal)
                cell.button.isEnabled = enabled
                return cell
            }
        }.disposed(by: disposeBag)
Run Code Online (Sandbox Code Playgroud)

到目前为止,这很好 - 这就是我的表单的样子:

在此输入图像描述

现在,我在这里遇到的实际问题是,当所有3个文本输入都不为空并且密码在两个密码文本字段中都相同时,我应该如何启用创建帐户按钮?换句话说,根据一个或多个其他单元格上发生的事件,将更改应用于单元格的正确方法是什么?

我的目标应该是formElementsVariable通过ViewModel 改变这一点,还是有更好的方法来实现我想要的?

San*_*eep 10

我建议您稍微更改ViewModel,以便您可以更好地控制文本字段中的更改.如果您从输入领域,如用户名,密码和确认创建流,可以认购的变化和反应,因此在任何你想要的方式.

以下是我重新编写代码以便处理文本字段更改的方法.

internal enum FormElement {
    case textInput(placeholder: String, variable: Variable<String>)
    case button(title: String)
}
Run Code Online (Sandbox Code Playgroud)

视图模型.

internal class ViewModel {

    let username = Variable("")
    let password = Variable("")
    let confirmation = Variable("")

    lazy var formElementsVariable: Driver<[FormElement]> = {
        return Observable<[FormElement]>.of([.textInput(placeholder: "username",
                                                          variable: username),
                                               .textInput(placeholder: "password",
                                                          variable: password),
                                               .textInput(placeholder: "password, again",
                                                          variable: confirmation),
                                               .button(title: "create account")])
            .asDriver(onErrorJustReturn: [])
    }()

    lazy var isFormValid: Driver<Bool> = {
        let usernameObservable = username.asObservable()
        let passwordObservable = password.asObservable()
        let confirmationObservable = confirmation.asObservable()

        return Observable.combineLatest(usernameObservable,
                                        passwordObservable,
                                        confirmationObservable) { [unowned self] username, password, confirmation in
                                            return self.validateFields(username: username,
                                                                       password: password,
                                                                       confirmation: confirmation)
            }.asDriver(onErrorJustReturn: false)
    }()

    fileprivate func validateFields(username: String,
                                    password: String,
                                    confirmation: String) -> Bool {

        guard username.count > 0,
            password.count > 0,
            password == confirmation else {
                return false
        }

        // do other validations here

        return true
    }
}
Run Code Online (Sandbox Code Playgroud)

视图控制器,

internal class ViewController: UIViewController {
    @IBOutlet var tableView: UITableView!

    fileprivate var viewModel = ViewModel()

    fileprivate let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.formElementsVariable.drive(tableView.rx.items) { [unowned self] (tableView: UITableView, index: Int, element: FormElement) in

                let indexPath = IndexPath(row: index, section: 0)

                switch element {

                case .textInput(let placeholder, let variable):

                    let cell = self.createTextInputCell(at: indexPath,
                                                        placeholder: placeholder)

                    cell.textField.text = variable.value
                    cell.textField.rx.text.orEmpty
                        .bind(to: variable)
                        .disposed(by: cell.disposeBag)
                    return cell

                case .button(let title):
                    let cell = self.createButtonCell(at: indexPath,
                                                     title: title)
                    self.viewModel.isFormValid.drive(cell.button.rx.isEnabled)
                        .disposed(by: cell.disposeBag)
                    return cell
                }
            }.disposed(by: disposeBag)
    }

    fileprivate func createTextInputCell(at indexPath:IndexPath,
                                         placeholder: String) -> TextInputTableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TextInputTableViewCell",
                                                 for: indexPath) as! TextInputTableViewCell
        cell.textField.placeholder = placeholder
        return cell
    }

    fileprivate func createButtonCell(at indexPath:IndexPath,
                                      title: String) -> ButtonInputTableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonInputTableViewCell",
                                                 for: indexPath) as! ButtonInputTableViewCell
        cell.button.setTitle(title, for: .normal)
        return cell
    }
}
Run Code Online (Sandbox Code Playgroud)

我们有三个不同的变量,基于它我们启用禁用按钮,你可以在这里看到流和rx运算符的功能.

我认为在我们的情况下,当它们改变很多像用户名,密码和密码字段时,将普通属性转换为Rx总是好的.您可以看到formElementsVariable变化不大,除了用于创建单元格的神奇tableview绑定外,它没有Rx的实际附加值.


小智 5

我认为您缺少rx内部适当的属性,FormElement这将使您能够将 UI 事件绑定到要在 ViewModel 中执行的验证。

首先FormElementtextInput应该公开一个文本 Variablebutton一个启用的 Driver。我做出这种区分是为了展示在第一种情况下您想要使用 UI 事件,而在第二种情况下您只想更新 UI。

enum FormElement {
   case textInput(placeholder: String, text: Variable<String?>)
   case button(title: String, enabled:Driver<Bool>, tapped:PublishRelay<Void>)
}
Run Code Online (Sandbox Code Playgroud)

我冒昧地添加了一个点击事件,使您能够在按钮最终启用时执行您的业务逻辑!

转到ViewModel,我只公开了View需要知道的内容,但在内部我应用了所有必要的运算符:

class FormViewModel {

    // what ViewModel exposes to view
    let formElementsVariable: Variable<[FormElement]>
    let registerObservable: Observable<Bool>

    init() {
        // form element variables, the middle step that was missing...
        let username = Variable<String?>(nil) // docs says that Variable will deprecated and you should use BehaviorRelay...
        let password = Variable<String?>(nil) 
        let passwordConfirmation = Variable<String?>(nil)
        let enabled: Driver<Bool> // no need for Variable as you only need to emit events (could also be an observable)
        let tapped = PublishRelay<Void>.init() // No need for Variable as there is no need for a default value

        // field validations
        let usernameValidObservable = username
            .asObservable()
            .map { text -> Bool in !(text?.isEmpty ?? true) }

        let passwordValidObservable = password
            .asObservable()
            .map { text -> Bool in text != nil && !text!.isEmpty && text!.count > 5 }

        let passwordConfirmationValidObservable = passwordConfirmation
            .asObservable()
            .map { text -> Bool in text != nil && !text!.isEmpty && text!.count > 5 }

        let passwordsMatchObservable = Observable.combineLatest(password.asObservable(), passwordConfirmation.asObservable())
            .map({ (password, passwordConfirmation) -> Bool in
                password == passwordConfirmation
            })

        // enable based on validations
        enabled = Observable.combineLatest(usernameValidObservable, passwordValidObservable, passwordConfirmationValidObservable, passwordsMatchObservable)
            .map({ (usernameValid, passwordValid, passwordConfirmationValid, passwordsMatch) -> Bool in
                usernameValid && passwordValid && passwordConfirmationValid && passwordsMatch // return true if all validations are true
            })
            .asDriver(onErrorJustReturn: false)

        // now that everything is in place, generate the form elements providing the ViewModel variables
        formElementsVariable = Variable<[FormElement]>([
            .textInput(placeholder: "username", text: username),
            .textInput(placeholder: "password", text: password),
            .textInput(placeholder: "password, again", text: passwordConfirmation),
            .button(title: "create account", enabled: enabled, tapped: tapped)
            ])

        // somehow you need to subscribe to register to handle for button clicks...
        // I think it's better to do it from ViewController because of the disposeBag and because you probably want to show a loading or something
        registerObservable = tapped
            .asObservable()
            .flatMap({ value -> Observable<Bool> in
                // Business login here!!!
                NSLog("Create account!!")
                return Observable.just(true)
            })
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,在您的View

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    private let disposeBag = DisposeBag()

    var formViewModel: FormViewModel = FormViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(UINib(nibName: "TextInputTableViewCell", bundle: nil), forCellReuseIdentifier: "TextInputTableViewCell")
        tableView.register(UINib(nibName: "ButtonTableViewCell", bundle: nil), forCellReuseIdentifier: "ButtonTableViewCell")

        // view subscribes to ViewModel observables...
        formViewModel.registerObservable.subscribe().disposed(by: disposeBag)

        formViewModel.formElementsVariable.asObservable()
            .bind(to: tableView.rx.items) {
                (tableView: UITableView, index: Int, element: FormElement) in
                let indexPath = IndexPath(row: index, section: 0)
                switch element {
                case .textInput(let placeholder, let defaultText):
                    let cell = tableView.dequeueReusableCell(withIdentifier: "TextInputTableViewCell", for: indexPath) as! TextInputTableViewCell
                    cell.textField.placeholder = placeholder
                    cell.textField.text = defaultText.value
                    // listen to text changes and pass them to viewmodel variable
                    cell.textField.rx.text.asObservable().bind(to: defaultText).disposed(by: self.disposeBag)
                    return cell
                case .button(let title, let enabled, let tapped):
                    let cell = tableView.dequeueReusableCell(withIdentifier: "ButtonTableViewCell", for: indexPath) as! ButtonTableViewCell
                    cell.button.setTitle(title, for: .normal)
                    // listen to viewmodel variable changes and pass them to button
                    enabled.drive(cell.button.rx.isEnabled).disposed(by: self.disposeBag)
                    // listen to button clicks and pass them to the viewmodel
                    cell.button.rx.tap.asObservable().bind(to: tapped).disposed(by: self.disposeBag)
                    return cell
                }
            }.disposed(by: disposeBag)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

希望我有所帮助!

附注。我主要是一名 Android 开发人员,但我发现您的问题(和赏金)很有趣,所以请原谅 (rx)swift 的任何粗糙边缘