0

I've got a class derived from UIView called ContentListView that goes like this:

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 { $0 }
        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)
    }
}

It is used in the view controller like this:

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[$0].isHidden = false
            })
            .disposed(by: disposeBag)
        
        contentListViews.forEach {
            $0.selectedContent
                .emit(onNext: { [weak self] in self?.onSelected(with: $0) })
                .disposed(by: disposeBag)
        }
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        contentListViews.forEach { $0.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 {
            $0.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)
    }
}

But when I tried to run it on iOS 11 and 12 simulators, it crashed. Although it worked on iOS 13 and 14. It crashed with this error:

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

The contentListViews in the storyboard themselves are ContentListView so the error seems weird. How do I solve this? It has been days and I'm stuck at this. :(

Thanks in advance.

EDIT: I've tried just using a singular ContentListView in the storyboard and deleted the other. And then I changed the outlet to:

@IBOutlet weak var myContentListView: ContentListView!

It produced another error:

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

And actually, when I scrolled the error messages, the same error message above was also there.

Also tried to do this instead:

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

    override func viewDidLoad() {
        super.viewDidLoad()

        myContentListView = myContentUIView as! ContentListView         
        ....
    } 

And it also produced the error above with other ones:

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).

I've tried all the answers in Unknown class in interface builder

But nothing worked so far.

  • If the view is being loaded from a storyboard it seems highly likely that the custom class isn’t set correctly or you have accidentally associated a view with the referencing collection outlet that isn’t a custom view – Paulw11 Sep 22 '21 at 06:57
  • Can you please check the dependencies, it seems that RxSwift has different versions which support Xcode 12 and a different version for Xcode < 12. https://github.com/ReactiveX/RxSwift#requirements – Md. Ibrahim Hassan Sep 22 '21 at 07:10
  • @Paulw11 If that's the case, it won't work on iOS 13 and 14 too, right? – Bawenang Rukmoko Pardian Putra Sep 22 '21 at 07:34
  • @Md.IbrahimHassan Ah, that makes sense. I do use XCode 12. Thanks. I'll have a look. – Bawenang Rukmoko Pardian Putra Sep 22 '21 at 07:34
  • I would expect the error to occur on all versions. You aren’t even in reactive code as far as I can see. It is a straight swift array exception. You should set a breakpoint and examine the array. See if you can workout what the errant `UIView` is. – Paulw11 Sep 22 '21 at 07:37
  • @Md.IbrahimHassan Unfortunately I'm on RxSwift 6.2. And it already supported XCode 12. So, back to square one. – Bawenang Rukmoko Pardian Putra Sep 22 '21 at 13:44
  • @Paulw11 If it occurred on all versions, then I wouldn't have stressed myself out for days. LOL :'( Anyway, how do you add a breakpoint in an internal process? The blocks except the `viewDidLoad` were from either UIKit or Foundation. – Bawenang Rukmoko Pardian Putra Sep 23 '21 at 01:04
  • The crash is almost certainly going to occur when you execute `contentListViews.forEach` since that is the point where the underlying `NSArray` will be bridged to `Array` and the element mismatches. Set the breakpoint in `viewDidLoad` and see what the array contains – Paulw11 Sep 23 '21 at 02:35
  • @Paulw11 I've edited to add more info and I think the main error is that the class `ContentListView` is not found on iOS 11 and 12 but not other versions by Interface Builder, for whatever reason. And when I looked at the debugger, the array does contain the `ContentListView` instances. So, it's not about NSArray. – Bawenang Rukmoko Pardian Putra Sep 23 '21 at 11:45
  • 2
    You definitely need to fix that "unknown class" - That is what is putting `UIView` in your array. Make sure that the "module" field in your view's custom class is blank and you have "Inherit module from target" checked. Sometimes simply deleting the custom class name and re-entering it is all that is required. – Paulw11 Sep 23 '21 at 11:54
  • @Paulw11 Like I said, I've done all of them but they don't work. I've even deleted the DerivedData, deleted all Carthage and Pods, and do a clean build. My last option would be to refactor the whole class into a few classes based on the type of data and use tableviews directly instead of using them in a generic `ContentListView`. Although that would mean I should also refactor other View Controllers because that class is being used in many places. I think those VCs also won't run anyway. Another option is just to skip iOS below 13 for the target. But that depends on the management's decision. – Bawenang Rukmoko Pardian Putra Sep 24 '21 at 00:56
  • It is probably a corruption in your storyboard file. Try viewing the storyboard as source and look for `TtC8Kolibree15ContentListView` – Paulw11 Sep 24 '21 at 01:16
  • @Paulw11 Hi, sorry it took a long time but I decided to just skip the bug fix and make another branch for the next feature story. Now I'm back in fixing the bug and I've found the cause. And it's not the storyboard. The storyboard is fine and working dandy. – Bawenang Rukmoko Pardian Putra Oct 06 '21 at 04:17

1 Answers1

0

After distancing myself from the bug and decided to do another feature story for a few days, I have found the cause of the bug. It was because I used PublishRelays and BehaviorRelays in the UIViewControllers and UIViews. They worked fine anywhere else, just not in UIKit clases. Observables, Signals, Drivers, Completeables, Singles, and Maybes can also work fine in UIViewControllers and UIViews. When I removed all relays in all the crashing UIViewControllers and UIViews and change them to use delegates instead, the crash doesn't appear anymore.