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