iosswiftrx-swiftrxdatasources

Preventing first responder to resign on tableview reload


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)

Solution

  • You need to disconnect the stream that causes table view updates from the stream that changes the addresses.

    And example might be something like:

    typealias ID = Int // an example
    typealias Address = String // probably needs to be more involved.
    
    struct Input { 
        let updateAddress: Observable<(ID, Address)>
    }
    
    struct Output { 
        let cells: Observable<[ID]>
        let addresses: Observable<[ID: Address]>
    }
    

    The table view would be bound to the cells while each cell would be bound to the addresses 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.