Как обновить ячейку табличного представления на основе значений из других ячеек с помощью MVVM / RxSwift?

Я новичок в RxSwift и пытаюсь научиться, создав простую форму регистрации. Я хочу реализовать это с помощью UITableView (в качестве упражнения, плюс он станет более сложным в будущем), поэтому в настоящее время я использую два типа ячеек:

  • TextInputTableViewCell только с UITextField
  • ButtonTableViewCell только с UIButton

Чтобы представить каждую ячейку, я создал перечисление, которое выглядит так:

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

и используйте его в Variable для подачи табличного представления:

    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)

Пока все хорошо - вот как выглядит моя форма:

Как обновить ячейку табличного представления на основе значений из других ячеек с помощью MVVM / RxSwift?

Теперь настоящая проблема, с которой я столкнулся, - это как я должен включить кнопку зарегистрироваться, когда все 3 текстовых ввода не пусты, а пароль одинаков в обоих текстовых полях пароля? Другими словами, как правильно применять изменения к ячейке на основе событий, происходящих в одной или нескольких других ячейках?

Должна ли моя цель состоять в том, чтобы изменить этот formElementsVariable с помощью ViewModel или есть какой-нибудь лучший способ добиться того, чего я хочу?

Вы видели мой ответ ниже?

Sandeep 15.03.2018 05:49

Я пробовал таким способом создать форму регистрации с помощью RXSwift и MVVM, но количество строк кода и сложность увеличились, поэтому я оставил эту идею.

Leena 15.03.2018 10:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
2
2 354
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Во-первых, вы можете попробовать RxDataSources, который является оболочкой RxSwift для TableViews. Во-вторых, чтобы ответить на ваш вопрос, я бы сделал изменение через ViewModel, то есть предоставил ViewModel для ячейки, а затем в ViewModel установил наблюдаемый объект, который будет обрабатывать проверку. Когда все это настроено, выполните combineLatest для всех проверяемых наблюдаемых ячеек.

Спасибо за ответ. Я рассматривал возможность использования RxDataSources, но, похоже, это было для случаев, когда мне нужны более сложные операции (например, добавление / удаление ячеек), поэтому я подумал, что оставлю это простым. Что касается остальной части вашего ответа, я был бы очень признателен, если бы вы могли предоставить образец кода / ссылки на что-то подобное, которое вы, возможно, видели в Интернете. Спасибо!

phi 10.03.2018 11:57

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

Учитывая, что вот один из способов сделать это. Это войдет в ViewModel и представит данные таблицы как наблюдаемые. Затем вы можете привязать текстовые поля для имени пользователя / пароля к свойствам (реле поведения), хотя, вероятно, лучше не раскрывать их как таковые в пользовательском интерфейсе (прятаться за свойствами)

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)
        }
}
BehaviorRelay следует указывать вместе с let. При каждом изменении в одном из полей будет событие next для Observable, возвращаемое tableData(), в результате чего ячейки табличного представления каждый раз перезагружаются.
Valérian 12.03.2018 20:27

Спасибо за ответ, это альтернативное решение, которое другие не рассматривали!

phi 18.03.2018 16:04
Ответ принят как подходящий

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

Вот как я немного реструктурировал ваш код для работы с изменениями в текстовых полях.

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

ViewModel.

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

У нас есть три разные переменные, на основе которых мы включаем кнопку отключения, вы можете увидеть здесь мощь операторов stream и rx.

Я думаю, что всегда полезно преобразовывать простые свойства в Rx, когда они сильно меняются, например, имя пользователя, пароль и passwordField в нашем случае. Вы можете видеть, что formElementsVariable не сильно меняется, у него нет реальной добавленной стоимости Rx, кроме волшебной привязки tableview для создания ячейки.

Я отслеживал этот вопрос последние 3 дня. Я также пытался дать ответ, и мой ответ был во многом таким же, но застрял в одной части. Но вы хорошо поработали. Очень хорошо!

Prabhjot Singh Gogana 13.03.2018 09:04

Я думаю, что вам не хватает соответствующих свойств rx внутри FormElement, которые позволят вам привязать события пользовательского интерфейса к проверкам, которые вы хотите выполнить в ViewModel.

Чтобы начать с FormElement, textInput должен предоставить текстVariable, а button - включеноDriver. Я сделал это различие, чтобы продемонстрировать, что в первом случае вы хотите использовать события пользовательского интерфейса, а во втором - просто обновить пользовательский интерфейс.

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

Хороший ответ, но вам нужно иметь мешок для утилизации для вашей ячейки (см. Ошибку № 3: adamborek.com/top-7-rxswift-mistakes). Усовершенствованием, позволяющим избежать необязательных строк, является привязка cell.textField.rx.text.orEmpty, которая заменяет nil пустой строкой.

Valérian 13.03.2018 22:37

Спасибо за решение, встроенные комментарии очень помогают! Однако мне пришлось согласиться с другим ответом, поскольку он очень похож и набрал больше голосов.

phi 18.03.2018 16:01

Другие вопросы по теме