5

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

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

为了表示每个单元格,我创建了一个如下所示的枚举:

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

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

通过这样的绑定:

    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)

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

在此处输入图像描述

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

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

4

4 回答 4

12

我建议您稍微更改一下 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
    }
}

视图控制器,

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 运算符的强大功能。

我认为在我们的例子中,当它们改变很多像用户名、密码和密码字段时,将普通属性转换为 Rx 总是好的。可以看到 formElementsVariable 变化不大,除了用于创建单元格的神奇的 tableview 绑定之外,它并没有 Rx 的真正附加值。

于 2018-03-13T02:00:58.483 回答
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>)
}

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

继续前进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 的任何粗糙边缘

于 2018-03-13T14:47:23.817 回答
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)
        }
}
于 2018-03-12T15:30:03.820 回答
2

首先,你可能想试试RxDataSources哪个是 TableViews 的 RxSwift 包装器。其次,为了回答你的问题,我会通过 ViewModel 进行更改——也就是说,为单元格提供一个 ViewModel,然后在 ViewModel 中设置一个可观察的来处理验证。当所有这些都设置好后,combineLatest对所有单元格的验证 observables 执行 a。

于 2018-03-10T10:48:23.393 回答