Предотвращение отставки первого респондента при перезагрузке tableview

#ios #swift #rx-swift #rxdatasources

#iOS #swift #rx-swift #rxdatasources

Вопрос:

Я использую RxSwift, и в настоящее время у меня есть список адресов, которые можно редактировать в строке

Т. е. пользователь нажимает на кнопку, и ячейка переходит в режим редактирования, по существу отображая несколько UITextFields

Теперь проблема в том, что, когда я привязываю ввод моего текстового поля к моей модели, TableView перезагружается, и, следовательно, клавиатура отключается, я также пробовал RxAnimatableDataSource, но безрезультатно, он по-прежнему отключает клавиатуру при каждом нажатии клавиши.

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

И вот моя привязка ViewController источника данных к 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 amp;amp; 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)
  

Ответ №1:

Вам необходимо отключить поток, который вызывает обновления представления таблицы, от потока, который изменяет адреса.

И примером может быть что-то вроде:

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

Представление таблицы будет привязано к cells , в то время как каждая ячейка будет привязана к addresses наблюдаемой с помощью compactMap для извлечения ее конкретной информации.

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

Полностью развернутый пример этого можно найти в моем примере RxMultiCounter.

Комментарии:

1. Да, это полностью работает! Я выбрал другой подход, чем ваше приложение, потому что мои функции ViewModels не доступны ViewController. Но сама идея работает.