无法将 UIView 的派生转换为 iOS 版本低于 13 的 UIView

A derivation of UIView cannot be casted to UIView on iOS version less than 13

我有一个派生自 UIView 的 class,名为 ContentListView,它是这样的:

import UIKit
import RxSwift
import RxRelay
import RxCocoa
import SwinjectStoryboard

class ContentListView: UIView {
    @IBInspectable var listName: String = ""
    @IBInspectable var headerHeight: CGFloat = 0
    @IBInspectable var footerHeight: CGFloat = 0

    @IBOutlet weak var tableView: UITableView!
    
    let viewDidLoad = PublishRelay<Void>()
    let viewDidAppear = PublishRelay<Void>()
    let reloadData = PublishRelay<Void>()
    let manualLoadData = PublishRelay<[ContentCellType]>()
    var initialContents: [ContentCellType]?
    private(set) lazy var selectedContent = selectedContentRelay.asSignal()
    
    private let disposeBag = DisposeBag()
    private let cellTypes = BehaviorRelay<[ContentCellType]>(value: [])
    private let didSelectIndexRelay = PublishRelay<Int>()
    private let selectedContentRelay = PublishRelay<ContentCellType>()

    private let contentNotFoundReuseId = R.reuseIdentifier.contentNotFoundErrorCell.identifier
    private let contentNotMatchReuseId = R.reuseIdentifier.contentNotMatchErrorCell.identifier
    private let myContentReuseId = R.reuseIdentifier.myContentTableViewCell.identifier
    private let associatedPracticeReuseId = R.reuseIdentifier.associatedPracticeTableViewCell.identifier
    private let associatedPracticeContentReuseId = R.reuseIdentifier.associatedPracticeContentTableViewCell.identifier
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        instantiateView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        
        instantiateView()
    }
    
    private func instantiateView() {
        guard let nib = R.nib.contentListView(owner: self) else { return }
        addSubview(nib, method: .fill)
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        
        setupTableView()
        setupViewModel()
    }
    
    private func setupTableView() {
        setupTableViewLayouts()
        registerCells()
        setupTableViewEvents()
    }
    
    private func setupViewModel() {
        let viewModel = createViewModel()
        
        viewModel.contents
            .drive(cellTypes)
            .disposed(by: self.disposeBag)
        
        viewModel.selectedContent
            .emit(to: selectedContentRelay)
            .disposed(by: disposeBag)
        
        viewDidLoad.asSignal()
            .emit(to: viewModel.viewDidLoad)
            .disposed(by: disposeBag)
        
        viewDidAppear.asSignal()
            .emit(to: viewModel.viewDidAppear)
            .disposed(by: disposeBag)
        
        reloadData.asSignal()
            .emit(to: viewModel.reloadData)
            .disposed(by: disposeBag)
        
        let loadInitialContents = Observable.just(initialContents).compactMap { [=11=] }
        Observable.merge(loadInitialContents,
                         manualLoadData.asObservable())
            .bind(to: viewModel.manualLoadData)
            .disposed(by: disposeBag)
        
        didSelectIndexRelay
            .bind(to: viewModel.didSelectIndex)
            .disposed(by: disposeBag)
        
    }
    
    private func createViewModel() -> ContentListViewModel {
        if let viewModel = SwinjectStoryboard.defaultContainer.resolve(ContentListViewModel.self, name: listName) {
            return viewModel
        } else {
            let viewModel = SwinjectStoryboard.defaultContainer.resolve(ContentListViewModel.self,
                                                                        name: "NoDataProvider")!
            return viewModel
        }
    }
    
    private func setupTableViewLayouts() {
        tableView.backgroundColor = R.color.grey91()
        tableView.separatorStyle = .none
    }
    
    private func registerCells() {
        tableView.register(UINib(resource: R.nib.contentNotFoundTableViewCell),
                           forCellReuseIdentifier: contentNotFoundReuseId)
        tableView.register(UINib(resource: R.nib.contentNotMatchTableViewCell),
                           forCellReuseIdentifier: contentNotMatchReuseId)
        tableView.register(UINib(resource: R.nib.myContentTableViewCell),
                           forCellReuseIdentifier: myContentReuseId)
        tableView.register(UINib(resource: R.nib.associatedPracticeTableViewCell),
                           forCellReuseIdentifier: associatedPracticeReuseId)
        tableView.register(UINib(resource: R.nib.associatedPracticeContentTableViewCell),
                           forCellReuseIdentifier: associatedPracticeContentReuseId)
    }
    
    private func setupTableViewEvents() {
        tableView.rx.setDelegate(self).disposed(by: disposeBag)
        
        cellTypes.asDriver()
            .drive(tableView.rx.items) { [weak self] tableView, _, element in
                return self?.createCell(tableView: tableView, element: element) ?? UITableViewCell()
            }
            .disposed(by: disposeBag)
        
        cellTypes.accept([.notFound])
    }
    
    private func createCell(tableView: UITableView, element: ContentCellType) -> UITableViewCell? {
        switch element {
        case .notFound: return tableView.dequeueReusableCell(withIdentifier: contentNotFoundReuseId)
        case .notMatch: return tableView.dequeueReusableCell(withIdentifier: contentNotMatchReuseId)
        case .content(data: _): return nil
        case .myContent(let data):
            let cell = tableView.dequeueReusableCell(withIdentifier: myContentReuseId) as? MyContentTableViewCell
            cell?.setup(with: data)
            return cell
        case .practice(let data):
            let cell = tableView.dequeueReusableCell(withIdentifier: associatedPracticeReuseId)
                as? AssociatedPracticeTableViewCell
            cell?.setup(with: data)
            return cell
        case .provider(let data):
            let cell = tableView.dequeueReusableCell(withIdentifier: associatedPracticeContentReuseId)
                as? AssociatedPracticeContentTableViewCell
            cell?.setup(with: data)
            return cell
        }
    }

}

extension ContentListView: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let type = cellTypes.value[indexPath.row]
        switch type {
        case .notFound, .notMatch: return 320
        case .myContent: return 440
        case .practice: return 76
        case .provider: return 412
        default: return 0
        }
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return headerHeight
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return footerHeight
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        didSelectIndexRelay.accept(indexPath.row)
    }
}

它在视图控制器中是这样使用的:

import UIKit
import RxSwift
import RxCocoa

class ContentsViewController: UIViewController, HideNavigationBarToggling {
    
    @IBOutlet var contentButtonViews: [ContentsButtonView]!
    @IBOutlet var contentListViews: [ContentListView]!

    private let disposeBag = DisposeBag()
    private var selectedPracticeName: String?
    private var selectedParam: MyContentViewParam?
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        hideListViews() //<<<<<<<<<<<<<< CRASH! 
        contentsButtonController.setup(with: contentButtonViews)
        contentsButtonController.activeSelectionIndex
            .drive(onNext: { [weak self] in
                self?.hideListViews()
                self?.contentListViews[[=12=]].isHidden = false
            })
            .disposed(by: disposeBag)
        
        contentListViews.forEach {
            [=12=].selectedContent
                .emit(onNext: { [weak self] in self?.onSelected(with: [=12=]) })
                .disposed(by: disposeBag)
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        contentListViews.forEach { [=12=].viewDidAppear.accept(()) }
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let providerVC = segue.destination as? AssociatedPracticeContentsViewController {
            providerVC.title = selectedPracticeName
        } else if let destinationNavigation = segue.destination as? KolibreeNavigationController,
                  let bottomVC = destinationNavigation.visibleViewController as? BottomMessageViewController {
            let messageSegue = segue as? SwiftMessagesBottomTabSegue
            messageSegue?.interactiveHide = false
            bottomVC.titleString = selectedParam?.title ?? ""
            bottomVC.setup = { [weak self] bottomMessage in
                if let pdfReader = bottomMessage as? PDFReaderMessageView,
                   let param = self?.selectedParam {
                    pdfReader.load(param: param)
                }
            }
        }
    }
    
    private func hideListViews() {
        contentListViews.forEach {
            [=12=].isHidden = true
        }
    }
    
    private func onSelected(with cellType: ContentCellType) {
        switch cellType {
        case .myContent(let param): openContent(for: param)
        case .practice(let param): showAssociatedPracticeContents(for: param)
        default: return
        }
    }
    
    private func openContent(for param: MyContentViewParam) {
        switch param.type {
        case .book:
            selectedParam = param
            performSegue(withIdentifier: R.segue.contentsViewController.openPdfReaderSegue.identifier, sender: nil)
        case .video, .audio:
            let avContentPlayerVC = AVContentPlayerViewController()
            present(avContentPlayerVC, animated: true) {
                avContentPlayerVC.load(param: param)
            }
        default: return
        }
    }
    
    private func showAssociatedPracticeContents(for param: AssociatedPracticeViewParam) {
        SelectedAssociatedPracticeStorageAdapter().store(param.practiceId)
        selectedPracticeName = param.practiceName
        performSegue(withIdentifier: R.segue.contentsViewController.showAssociatedPracticeContents.identifier,
                     sender: nil)
    }
}

但是当我尝试在 iOS 11 和 12 模拟器上 运行 它时,它崩溃了。尽管它在 iOS 13 和 14 上工作。它因以下错误而崩溃:

Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ContentListView but found UIView: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1001.0.82.4/swift/stdlib/public/core/ArrayBuffer.swift, line 346
2021-09-22 13:24:27.624568+0700 Kolibree[16970:513272] Precondition failed: NSArray element failed to match the Swift Array Element type
Expected ContentListView but found UIView: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1001.0.82.4/swift/stdlib/public/core/ArrayBuffer.swift, line 346

情节提要中的 contentListViews 本身是 ContentListView 所以错误看起来很奇怪。我该如何解决这个问题?已经好几天了,我一直坚持这一点。 :(

提前致谢。

编辑: 我试过在情节提要中只使用一个单数 ContentListView 并删除另一个。然后我将插座更改为:

@IBOutlet weak var myContentListView: ContentListView!

它产生了另一个错误:

2021-09-23 13:59:05.669493+0700 Kolibree[14267:377067] Unknown class _TtC8Kolibree15ContentListView in Interface Builder file.

实际上,当我滚动错误消息时,也出现了上面相同的错误消息。

也尝试这样做:

    @IBOutlet weak var myContentUIView: UIView!
    private var myContentListView: ContentListView!

    override func viewDidLoad() {
        super.viewDidLoad()

        myContentListView = myContentUIView as! ContentListView         
        ....
    } 

并且它还与其他错误一起产生了上述错误:

Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).
2021-09-23 15:29:12.151228+0700 Kolibree[15518:434665] Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).
Could not cast value of type 'UIView' (0x10e6dbff8) to 'Kolibree.ContentListView' (0x106d922a0).

我已经尝试了 Unknown class in interface builder

中的所有答案

但到目前为止没有任何效果。

在与错误保持距离并决定用几天时间再做一个专题报道后,我找到了错误的原因。这是因为我在 UIViewControllers 和 UIViews 中使用了 PublishRelaysBehaviorRelays。他们在其他任何地方都工作得很好,只是在 UIKit 类中不行。 ObservablesSignalsDriversCompleteablesSinglesMaybes 也可以在 UIViewController 和 UIView 中正常工作。当我删除所有崩溃的 UIViewControllersUIViews 中的所有中继并将它们更改为使用代表时,崩溃不再出现。