RxSwiftCommunity / RxDataSources

UITableView and UICollectionView Data Sources for RxSwift (sections, animated updates, editing ...)
MIT License
3.09k stars 494 forks source link

Crash after providing accessibilityScrollStatus(for scrollView: UIScrollView) #398

Open Ma-He opened 3 years ago

Ma-He commented 3 years ago

We are trying to override the default iOS VoiceOver read out during the scrolling of a UITableView with RxTableViewSectionedReloadDataSource binded to it. After extending our ViewController to UIScrollViewAccessibilityDelegate and implementing

func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? {
    return "Some status"
}

we will receive a crash when VoiceOver is enabled and we are trying to scroll the list.

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[RxCocoa.RxTableViewDelegateProxy accessibilityScrollStatusForScrollView:]: unrecognized selector sent to instance 0x282a966f0'

To recreate the error, you can simply display this VoiceOverScrollStatusViewController and scroll down the list with VoiceOver enabled.

import Foundation
import UIKit
import RxSwift
import RxCocoa
import RxDataSources

struct CustomData {
    var anInt: Int
    var aString: String
}

struct SectionOfCustomData {
    var header: String
    var items: [Item]
}

extension SectionOfCustomData: SectionModelType {
    typealias Item = CustomData

    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class VoiceOverScrollStatusViewController: UIViewController {
    private let tableView: UITableView
    private let disposeBag = DisposeBag()

    let rxDataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData> { (_, tableView, _, itemSource) -> UITableViewCell in
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else {
            return UITableViewCell()
        }

        cell.textLabel?.text = "Cell \(itemSource.anInt): \(itemSource.aString)"
        return cell
    }

    init() {
        if #available(iOS 13.0, *) {
            tableView = UITableView(frame: .zero, style: .insetGrouped)
        } else {
            tableView = UITableView(frame: .zero, style: .grouped)
        }

        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - view lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        setupTableView()

        rxDataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }

        let sections = [
            SectionOfCustomData(header: "First Section", items: [.init(anInt: 1, aString: "First"),
                                                                 .init(anInt: 2, aString: "Zweiter"),
                                                                 .init(anInt: 3, aString: "Third")]),
            SectionOfCustomData(header: "Second Section", items: [.init(anInt: 1, aString: "First"),
                                                                 .init(anInt: 2, aString: "Zweiter")]),
            SectionOfCustomData(header: "Third Section", items: [.init(anInt: 1, aString: "First"),
                                                                 .init(anInt: 2, aString: "Zweiter"),
                                                                 .init(anInt: 3, aString: "Third")]),
            SectionOfCustomData(header: "Fourth Section", items: [.init(anInt: 1, aString: "First")]),
            SectionOfCustomData(header: "Fifth Section", items: [.init(anInt: 1, aString: "First"),
                                                                 .init(anInt: 2, aString: "Second"),
                                                                 .init(anInt: 3, aString: "Third")])
        ]

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

    private func setupTableView() {
        tableView.backgroundColor = UIColor(hex: "#F3F5F8")
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)

        if #available(iOS 11.0, *) {
            let guide = view.safeAreaLayoutGuide
            tableView.topAnchor.constraint(equalTo: guide.topAnchor).isActive = true
            tableView.bottomAnchor.constraint(equalTo: guide.bottomAnchor).isActive = true
            tableView.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
            tableView.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
        } else {
            tableView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor).isActive = true
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
        }

        tableView.delegate = self
    }
}

extension VoiceOverScrollStatusViewController: UITableViewDelegate {

}

extension VoiceOverScrollStatusViewController: UIScrollViewAccessibilityDelegate {
    func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? {
        return "Some status"
    }
}
ddosang commented 7 months ago

I have same issue.

i6abesz commented 3 months ago

This works:

extension RxScrollViewDelegateProxy: UIScrollViewAccessibilityDelegate {
    func accessibilityScrollStatus(for scrollView: UIScrollView) -> String? {
        return "Some status"
    }
}