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.