【发布时间】:2021-11-15 15:17:50
【问题描述】:
我有一个派生自 UIView 的类,名为 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 { $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)
}
}
在视图控制器中使用如下:
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)
}
}
但是当我尝试在 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中的所有答案
但到目前为止没有任何效果。
【问题讨论】:
-
如果视图是从情节提要加载的,很可能是自定义类设置不正确,或者您不小心将视图与不是自定义视图的引用集合出口相关联
-
你能检查一下依赖关系吗,看起来 RxSwift 有不同的版本支持 Xcode 12 和 Xcode github.com/ReactiveX/RxSwift#requirements
-
@Paulw11 如果是这样的话,它在 iOS 13 和 14 上也不会工作,对吧?
-
@Md.IbrahimHassan 啊,这很有道理。我确实使用 XCode 12。谢谢。我去看看。
-
你肯定需要修复那个“未知类”——这就是将
UIView放入你的数组的原因。确保视图自定义类中的“模块”字段为空白,并且选中了“从目标继承模块”。有时只需删除自定义类名并重新输入即可。