I'm using RxSwift and currently, I have a list of addresses that could be edited inline
I.E: The user clicks on a button and the cell is transformed into editing mode essentially displaying a few UITextFields
Now the problem is that when I bind the input of my TextField to my model the TableView gets reloaded and therefore keyboard dismisses, I've also tried RxAnimatableDataSource but no avail it still dismisses the keyboard on each keystroke.
ViewModel:
class UpdateAddressViewModel: ViewModel, ViewModelType {
struct Input {
let viewDidLoad: AnyObserver<Void>
let selectItemTrigger: AnyObserver<Int>
let editButtonTrigger: AnyObserver<Int>
let editTrigger: AnyObserver<Int>
let firstNameIndexed: AnyObserver<(String, Int)>
let lastNameIndexed: AnyObserver<(String, Int)>
let address1Indexed: AnyObserver<(String, Int)>
let address2Indexed: AnyObserver<(String, Int)>
let zipIndexed: AnyObserver<(String, Int)>
let cityIndexed: AnyObserver<(String, Int)>
let stateIndexed: AnyObserver<(String, Int)>
let phoneIndexed: AnyObserver<(String, Int)>
}
struct Output {
let addresses: Driver<[AddressViewModel]>
let reloadAndScroll: Driver<(Int, Bool)>
let showMenu: Driver<Int>
let error: Driver<Error>
}
private(set) var input: Input!
private(set) var output: Output!
//Input
private let viewDidLoad = PublishSubject<Void>()
private let editButtonTrigger = PublishSubject<Int>()
private let editTrigger = PublishSubject<Int>()
private let firstNameIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let lastNameIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let address1Indexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let address2Indexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let zipIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let cityIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let stateIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
private let phoneIndexed = ReplaySubject<(String, Int)>.create(bufferSize: 1)
//Output
private let addresses = BehaviorRelay<[AddressViewModel]>(value: [AddressViewModel()])
override init() {
super.init()
observeViewDidLoad()
observeEditAddress()
.bind(to: addresses)
.disposed(by: disposeBag)
firstNameIndexed
.withLatestFrom(addresses) { firstNameIndexed, viewModels -> (String, Int, [AddressViewModel]) in
let (firstName, index) = firstNameIndexed
return (firstName, index, viewModels)
}
.map { firstName, index, viewModels in
var viewModels = viewModels
viewModels[index].address.firstName = firstName
return viewModels
}
.bind(to: addresses)
.disposed(by: disposeBag)
input = Input(
viewDidLoad: viewDidLoad.asObserver(),
selectItemTrigger: selectItemTrigger.asObserver(),
editButtonTrigger: editButtonTrigger.asObserver(),
editTrigger: editTrigger.asObserver(),
firstNameIndexed: firstNameIndexed.asObserver(),
lastNameIndexed: lastNameIndexed.asObserver(),
address1Indexed: address1Indexed.asObserver(),
address2Indexed: address2Indexed.asObserver(),
zipIndexed: zipIndexed.asObserver(),
cityIndexed: cityIndexed.asObserver(),
stateIndexed: stateIndexed.asObserver(),
phoneIndexed: phoneIndexed.asObserver()
)
output = Output(
addresses: addresses.asDriver(onErrorJustReturn: []),
reloadAndScroll: reloadAndScroll,
showMenu: editButtonTrigger.asDriverOnErrorJustComplete(),
error: errorTracker.asDriver()
)
}
private func observeViewDidLoad() {
//Loading work here
}
private func observeEditAddress() -> Observable<[AddressViewModel]> {
editTrigger
.withLatestFrom(addresses) { index, viewModels in
return (index, viewModels)
}
.map { index, viewModels in
var viewModels = viewModels
for currentIndex in viewModels.indices {
viewModels[currentIndex].isSelected = currentIndex == index
viewModels[currentIndex].isEditing = currentIndex == index
if currentIndex == index {
viewModels[currentIndex].copyAddress()
}
}
return viewModels
}
}
}
And here's my ViewController binding of data source to tableview
viewModel
.output
.addresses
.drive(tableView.rx.items) { [weak self] tableView, row, viewModel in
guard let self = self else { return UITableViewCell() }
let indexPath = IndexPath(row: row, section: 0)
if viewModel.address.rechargeId == nil && viewModel.address.shopifyId == nil {
let cell: NewAddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
return cell
} else if viewModel.isEditing {
let cell: UpdateAddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
cell
.rx
.firstName
.map { ($0, row) }
.bind(to: self.viewModel.input.firstNameIndexed)
.disposed(by: cell.disposeBag)
return cell
} else {
let cell: AddressCell = tableView.dequeueCell(for: indexPath)
cell.configure(viewModel: viewModel)
cell
.rx
.editButtonTapped
.map { _ in return row }
.bind(to: self.viewModel.input.editButtonTrigger)
.disposed(by: cell.disposeBag)
return cell
}
}
.disposed(by: disposeBag)
You need to disconnect the stream that causes table view updates from the stream that changes the addresses.
And example might be something like:
The table view would be bound to the
cells
while each cell would be bound to theaddresses
observable with a compactMap to extract its particular information.That way, you can update a particular cell without reloading any of the cells in the table view.
A fully fleshed out example of this can be found in my RxMultiCounter example.