cleanrun
cleanrun

Reputation: 746

How to retain UITextField's value inside a UITableViewCell when using RxSwift?

I'm making an app which has a form in it. What I did is I use a UITableView to make the form with some sections in it. Each cell has a UITextField to get the user's input. Since I'm using RxSwift, I bind every textfield inside the cell to a BehaviorRelay to my ViewModel class. Unfortunately, it has a really strange behavior everytime the user inputs something to each cell. For example (as shown below) every time the user inputs some value to the first cell, after the user scrolls down, the last cell in the table view has the same value (Note that the user hasn't scroll the page yet, hence hasn't input any value to the last cell). The second example is when I input some value in the last 3 cells, the first 3 cell's values changed as well.

enter image description here

Here's how I manage to achieve this:

My custom cell class:

class FormTableViewCell: UITableViewCell {
    
    @IBOutlet weak var titleLbl: UILabel!
    @IBOutlet weak var valueTf: UITextField!

    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    func configureCell(title: String, placeholder: String, keyboardType: UIKeyboardType? = .default) {
        titleLbl.text = title
        valueTf.placeholder = placeholder
        valueTf.keyboardType = keyboardType ?? .default
    }
    
    func getValueAsDriver() -> Driver<String> {
        valueTf.rx.text.orEmpty.asDriver()
    }
    
}

How I configure each cell in tableView(_:cellForRowAt:) function in my UIViewController class (I have around 15 cells total in my app):

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let formCell = tableView.dequeueReusableCell(withIdentifier: Config.FORM_TABLEVIEWCELL_ID) as! FormTableViewCell
    if indexPath.section == 1 {
        if indexPath.row == 0 {
            formCell.configureCell(title: "Some title", placeholder: "Some placeholder", keyboardType: .decimalPad)
            viewModel.bindValue(from: formCell.getValueAsDriver())
            return formCell
        } else if indexPath.row == 1 {
            // Do pretty much the same thing as before
        }
    } else if indexPath.section == 2 {
        // Do pretty much the same thing as before
    } ...
    
    return UITableViewCell()
}

How I bind the textfield value in my ViewModel class:

final class ViewModel {
    private let _someValue = BehaviorRelay<String>(value: "")

    func bindValue(from driver: Driver<String>) {
        driver.distinctUntilChanged().drive(onNext: { [unowned self] value in
            _someValue.accept(value)
        }).disposed(by: disposeBag)
    }
}

My question is how do keep the values for each textfield inside the cell when the user scrolls the tableview? Also if you have better approach please let me know. Thank you.

Upvotes: 3

Views: 843

Answers (2)

Daniel T.
Daniel T.

Reputation: 33967

You keep the values for each text field by binding the text field to behavior relay and using take(1). Something like:

viewModel._someValue
    .take(1)
    .bind(to: valueTf.rx.text)
    .disposed(by: cellDisposeBag)

Notice in the above that your cell needs a dispose bag. In that cell you also have to release the disposeBag and create a new one in the prepareForReuse method.

override func prepareForReuse() {
    super.prepareForReuse()
    disposeBag = DisposeBag()
}

Some other general notes... If you feel the need to put a disposeBag in your view model, you are probably doing something wrong. Your view model should not be a class. It should be a struct or better yet a single function.


A better approach would be to use the RxDataSources library and get rid of the huge "if/else if" chain in your view controller. I would write it more like this:

final class ExampleViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    let viewModel = ViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let dataSource = RxTableViewSectionedReloadDataSource<FormSectionModel>(
            configureCell: { [formValues = viewModel.formValues] _, tableView, _, item in
                let formCell = tableView.dequeueReusableCell(withIdentifier: Config.FORM_TABLEVIEWCELL_ID) as! FormTableViewCell
                formCell.configureCell(item: item, formValues: formValues)
                return formCell
            },
            titleForHeaderInSection: { dataSource, index in
                dataSource.sectionModels[index].model
            }
        )

        Observable.just(viewModel.form)
            .bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
}

final class FormTableViewCell: UITableViewCell {

    @IBOutlet weak var titleLbl: UILabel!
    @IBOutlet weak var valueTf: UITextField!
    var disposeBag = DisposeBag()

    override func prepareForReuse() {
        super.prepareForReuse()
        disposeBag = DisposeBag()
    }

    func configureCell(item: FormItem, formValues: BehaviorSubject<[FormID : String]>) {
        titleLbl.text = item.title
        valueTf.placeholder = item.placeholder
        valueTf.keyboardType = item.keyboardType

        formValues
            .map { $0[item.formID] }
            .take(1)
            .bind(to: valueTf.rx.text)
            .disposed(by: disposeBag)

        valueTf.rx.text
            .withLatestFrom(formValues) { entry, values in
                with(values) {
                    $0[item.formID] = entry ?? ""
                }
            }
            .bind(to: formValues)
            .disposed(by: disposeBag)
    }
}

struct ViewModel {
    let form: [FormSectionModel] = [
        FormSectionModel(model: "PELANGGAN", items: [
            FormItem(formID: .pelanggan, title: "Pelanggan", placeholder: "")
        ]),
        FormSectionModel(model: "PUNGGUNG", items: [
            FormItem(formID: .pangangPunggung, title: "Pangang Punggung", placeholder: "eg 20", keyboardType: .numberPad),
            FormItem(formID: .tinggiPunggung, title: "Tinggi Punggung", placeholder: "eg 20", keyboardType: .numberPad)
        ]),
        FormSectionModel(model: "PUNDAK", items: [
            FormItem(formID: .jarakPundak, title: "Jarak Pundak", placeholder: "eg 20", keyboardType: .numberPad),
            FormItem(formID: .lebarPundak, title: "Lebar Pundak", placeholder: "eg 20", keyboardType: .numberPad),
            FormItem(formID: .tinggiPundak, title: "Tinggi Pundak", placeholder: "eg 20", keyboardType: .numberPad),
        ])
    ]
    let formValues = BehaviorSubject<[FormID: String]>(value: [:])
}

typealias FormSectionModel = SectionModel<String, FormItem>

struct FormItem {
    let formID: FormID
    let title: String
    let placeholder: String
    let keyboardType: UIKeyboardType
}

extension FormItem {
    init(formID: FormID, title: String, placeholder: String) {
        self.formID = formID
        self.title = title
        self.placeholder = placeholder
        self.keyboardType = .default
    }
}

enum FormID {
    case pelanggan
    case pangangPunggung
    case tinggiPunggung
    case jarakPundak
    case lebarPundak
    case tinggiPundak
}

func with<T>(_ item: T, _ fn: (inout T) -> Void) -> T {
    var item = item
    fn(&item)
    return item
}

Upvotes: 3

jatin fl
jatin fl

Reputation: 175

you can avoid it using prepareForReuse

just add this in FormTableViewCell

override func prepareForReuse() {
    super.prepareForReuse()
    valueTf.text = ""
}

::UPDATE::

create a action in cell like this

var textChanged: ((String) -> Void)? 

than declare action method & textfield delegate method in cell

func textChanged(action: @escaping (String) -> Void) {
    self.textChanged = action
}

func textFieldDidEndEditing(_ textField: UITextField) {
    textChanged?(textField.text)
}

final step code in cellForRowAt

cell.textChanged {[weak tableView, weak self] newText in
        self?.arr[indexPath.row] = newText
        cell.valueTf.text = newText
        
        DispatchQueue.main.async {
            tableView?.beginUpdates()
            tableView?.endUpdates()
        }
    }

Upvotes: 1

Related Questions