diff --git a/Sources/Classes/Search/BaseSearchViewController.swift b/Sources/Classes/Search/BaseSearchViewController.swift index 7eca4512..1d4d97e6 100644 --- a/Sources/Classes/Search/BaseSearchViewController.swift +++ b/Sources/Classes/Search/BaseSearchViewController.swift @@ -26,24 +26,21 @@ import RxSwift open class BaseSearchViewController: UIViewController, ConfigurableController -where ViewModel: BaseSearchViewModel, CustomView: UIView, CustomView: TableViewHolder { + CustomView: UIView & TableViewHolder>: BaseCustomViewController +where ViewModel: BaseSearchViewModel { // MARK: - Properties - public let viewModel: ViewModel private let disposeBag = DisposeBag() - let customView = CustomView() - private let searchResultsViewController = ResultsVC() + private let searchResultsViewController: UIViewController & SearchResultsViewController private lazy var searchController = UISearchController(searchResultsController: searchResultsViewController) private var didEnterText = false // MARK: - Initialization - init(viewModel: ViewModel) { - self.viewModel = viewModel - super.init(nibName: nil, bundle: nil) + init(viewModel: ViewModel, searchResultsController: UIViewController & SearchResultsViewController) { + self.searchResultsViewController = searchResultsController + super.init(viewModel: viewModel) } required public init?(coder aDecoder: NSCoder) { @@ -58,11 +55,11 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C // MARK: - Configurable Controller - public func configureBarButtons() { + open override func configureBarButtons() { // override in subclass } - public func bindViews() { + open override func bindViews() { viewModel.itemsViewModelsDriver .drive(onNext: { [weak self] viewModels in self?.handle(itemViewModels: viewModels) @@ -86,7 +83,7 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C .disposed(by: disposeBag) } - public func addViews() { + open override func addViews() { if #available(iOS 11.0, *) { navigationItem.searchController = searchController } else { @@ -95,19 +92,20 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C searchController.view.addSubview(statusBarView) } - public func configureAppearance() { + open override func configureAppearance() { definesPresentationContext = true customView.tableView.tableHeaderView?.backgroundColor = searchBarColor } - public func localize() { + open override func localize() { searchController.searchBar.placeholder = searchBarPlaceholder } // MARK: - Search Controller Functionality - func createRows(from itemsViewModels: [ItemViewModel]) -> [Row] { - fatalError("createRows(from:) has not been implemented") + open func createRows(from itemsViewModels: [ItemViewModel]) -> [Row] { + assertionFailure("createRows(from:) has not been implemented") + return [] } var searchBarPlaceholder: String { @@ -118,7 +116,7 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C return .gray } - var statusBarView: UIView { + open var statusBarView: UIView { let statusBarSize = UIApplication.shared.statusBarFrame.size let statusBarView = UIView(frame: CGRect(x: 0, y: 0, @@ -162,7 +160,7 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C } func handle(searchResultsState state: SearchResultsViewControllerState) { - searchResultsViewController.update(from: state) + searchResultsViewController.update(for: state) } func handle(searchText: String?) { @@ -170,11 +168,12 @@ where ViewModel: BaseSearchViewModel, CustomView: UIView, C } private func setTableViewInsets() { - if !didEnterText { - didEnterText = true - searchResultsViewController.resultsView?.tableView.contentInset = tableViewInsets - searchResultsViewController.resultsView?.tableView.scrollIndicatorInsets = tableViewInsets + guard !didEnterText else { + return } + didEnterText = true + searchResultsViewController.searchResultsView.tableView.contentInset = tableViewInsets + searchResultsViewController.searchResultsView.tableView.scrollIndicatorInsets = tableViewInsets } } diff --git a/Sources/Classes/Search/BaseSearchViewModel.swift b/Sources/Classes/Search/BaseSearchViewModel.swift index 120ef805..42ee9ec1 100644 --- a/Sources/Classes/Search/BaseSearchViewModel.swift +++ b/Sources/Classes/Search/BaseSearchViewModel.swift @@ -23,6 +23,22 @@ import RxSwift import RxCocoa +private protocol OptionalType { + associatedtype Wrapped + var optional: Wrapped? { get } +} +extension Optional: OptionalType { + public var optional: Wrapped? { return self } +} + +private extension Observable where Element: OptionalType { + func filterNil() -> Observable { + return flatMap { value -> Observable in + value.optional.map { .just($0) } ?? .empty() + } + } +} + open class BaseSearchViewModel: GeneralDataLoadingViewModel<[Item]> { typealias ItemsList = [Item] @@ -30,7 +46,7 @@ open class BaseSearchViewModel: GeneralDataLoadingViewModel private let searchTextRelay = BehaviorRelay(value: "") init(dataSource: Single) { - super.init(dataSource: dataSource, emptyResultChecker: { _ in false }) + super.init(dataSource: dataSource, emptyResultChecker: { $0.isEmpty }) } open var itemsViewModelsDriver: Driver<[ItemViewModel]> { @@ -38,13 +54,7 @@ open class BaseSearchViewModel: GeneralDataLoadingViewModel .map { [weak self] items in self?.viewModels(from: items) } - .flatMap({ element -> Observable<[ItemViewModel]> in - if let value = element { - return .just(value) - } else { - return .empty() - } - }) + .filterNil() .share(replay: 1, scope: .forever) .asDriver(onErrorDriveWith: .empty()) } @@ -58,18 +68,12 @@ open class BaseSearchViewModel: GeneralDataLoadingViewModel .map { [weak self] items in self?.viewModels(from: items) } - .flatMap({ element -> Observable<[ItemViewModel]> in - if let value = element { - return .just(value) - } else { - return .empty() - } - }) + .filterNil() .share(replay: 1, scope: .forever) .asDriver(onErrorDriveWith: .empty()) } - func viewModel(from item: Item) -> ItemViewModel { + open func viewModel(from item: Item) -> ItemViewModel { fatalError("viewModel(from:) has not been implemented") } @@ -92,39 +96,15 @@ open class BaseSearchViewModel: GeneralDataLoadingViewModel var loadingResultObservable: Observable { return loadingStateDriver .asObservable() - .map { state -> ResultType? in - guard case let .result(data, _) = state else { - return nil - } - - return data - } - .flatMap({ element -> Observable in - if let value = element { - return .just(value) - } else { - return .empty() - } - }) + .map { $0.result } + .filterNil() } var loadingErrorObservable: Observable { return loadingStateDriver .asObservable() - .map { state -> Error? in - guard case let .error(error) = state else { - return nil - } - - return error - } - .flatMap({ element -> Observable in - if let value = element { - return .just(value) - } else { - return .empty() - } - }) + .map { $0.error } + .filterNil() } var firstLoadingResultObservable: Single { diff --git a/Sources/Protocols/Controllers/SearchResultsViewController.swift b/Sources/Protocols/Controllers/SearchResultsViewController.swift index db0c1b38..59d84419 100644 --- a/Sources/Protocols/Controllers/SearchResultsViewController.swift +++ b/Sources/Protocols/Controllers/SearchResultsViewController.swift @@ -20,10 +20,10 @@ // THE SOFTWARE. // -public protocol SearchResultsViewController where Self: UIViewController { +public protocol SearchResultsViewController { - var resultsView: TableViewHolder? { get set } + var searchResultsView: TableViewHolder { get set } - func update(from state: SearchResultsViewControllerState) + func update(for state: SearchResultsViewControllerState) }