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

时间:2018-03-09 15:49:55

标签: ios swift mvvm rx-swift rx-cocoa

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

  • 只有TextInputTableViewCell
  • UITextField
  • 只有ButtonTableViewCell
  • UIButton

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

enum FormElement {
    case textInput(placeholder: String, text: String?)
    case button(title: String, enabled: Bool)
}

并在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)
        ])

通过这样绑定:

    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)

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

enter image description here

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

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

4 个答案:

答案 0 :(得分:9)

我建议您稍微更改ViewModel,以便可以更好地控制文本字段中的更改。如果您从输入字段(如用户名,密码和确认)创建流,则可以订阅更改并以您想要的任何方式对其做出反应。

以下是我为了处理文本字段中的更改而重新编写代码的方法。

internal enum FormElement {
    case textInput(placeholder: String, variable: Variable<String>)
    case button(title: String)
}

视图模型。

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

的ViewController,

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

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

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

答案 1 :(得分:4)

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

FormElement开始,textInput应该公开文本 Variable并启用button > Driver。我做了这个区别来展示在第一种情况下你想要使用UI事件,而在第二种情况下你只想更新UI。

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

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

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

最后,在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)
        }
    }
}

希望我帮忙!

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

答案 2 :(得分:3)

你最好一次发送表数据而不是一次发送一行,因为否则你无法真正区分a)下一个事件是新行还是b)下一个事件是刷新一个我已经展示了一行。

鉴于这是一种方法。这将放在ViewModel中,并将表数据作为可观察对象呈现。然后,您可以将用户名/密码的文本字段绑定到属性(行为中继),但可能更好的是不将它们暴露给UI(隐藏在属性后面)

var userName = BehaviorRelay<String>(value: "")
var password1 = BehaviorRelay<String>(value: "")
var password2 = BehaviorRelay<String>(value: "")

struct LoginTableValues {
    let username: String
    let password1: String
    let password2: String
    let createEnabled: Bool
}

func tableData() -> Observable<LoginTableValues> {
    let createEnabled = Observable.combineLatest(userName.asObservable(), password1.asObservable(), password2.asObservable())
        .map { (username: String, password1: String, password2: String) -> Bool in
            return !username.isEmpty &&
                !password1.isEmpty &&
                password1 == password2
        }

    return Observable.combineLatest(userName.asObservable(), password1.asObservable(), password2.asObservable(), createEnabled)
        .map { (arg: (String, String, String, Bool)) -> LoginTableValues in
            let (username, password1, password2, createEnabled) = arg
            return LoginTableValues(username: username, password1: password1, password2: password2, createEnabled: createEnabled)
        }
}

答案 3 :(得分:2)

首先,您可能想尝试var mongoose = require("../../connection"); var Schema = mongoose.Schema; var userSchema = new Schema({userid:String, password:String
}); var User = mongoose.model("onlineusers",userSchema); module.exports = User;
这是TableViews的RxSwift包装器。其次,为了回答你的问题,我会通过ViewModel完成更改 - 也就是说,为单元格提供ViewModel,然后在ViewModel中设置一个将处理验证的observable。完成所有这些操作后,在所有单元格的验证可观察对象上执行RxDataSources