Reputation: 746
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.
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
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
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