// // UITableView+Rx.swift // RxCocoa // // Created by Krunoslav Zaher on 4/2/15. // Copyright © 2015 Krunoslav Zaher. All rights reserved. // #if os(iOS) || os(tvOS) import RxSwift import UIKit #if swift(>=4.2) public typealias UITableViewCellEditingStyle = UITableViewCell.EditingStyle #endif // Items extension Reactive where Base: UITableView { /** Binds sequences of elements to table view rows. - parameter source: Observable sequence of items. - parameter cellFactory: Transform between sequence elements and view cells. - returns: Disposable object that can be used to unbind. Example: let items = Observable.just([ "First Item", "Second Item", "Third Item" ]) items .bind(to: tableView.rx.items) { (tableView, row, element) in let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")! cell.textLabel?.text = "\(element) @ row \(row)" return cell } .disposed(by: disposeBag) */ public func items (_ source: O) -> (_ cellFactory: @escaping (UITableView, Int, S.Iterator.Element) -> UITableViewCell) -> Disposable where O.E == S { return { cellFactory in let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper(cellFactory: cellFactory) return self.items(dataSource: dataSource)(source) } } /** Binds sequences of elements to table view rows. - parameter cellIdentifier: Identifier used to dequeue cells. - parameter source: Observable sequence of items. - parameter configureCell: Transform between sequence elements and view cells. - parameter cellType: Type of table view cell. - returns: Disposable object that can be used to unbind. Example: let items = Observable.just([ "First Item", "Second Item", "Third Item" ]) items .bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in cell.textLabel?.text = "\(element) @ row \(row)" } .disposed(by: disposeBag) */ public func items (cellIdentifier: String, cellType: Cell.Type = Cell.self) -> (_ source: O) -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void) -> Disposable where O.E == S { return { source in return { configureCell in let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper { tv, i, item in let indexPath = IndexPath(item: i, section: 0) let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell configureCell(i, item, cell) return cell } return self.items(dataSource: dataSource)(source) } } } /** Binds sequences of elements to table view rows using a custom reactive data used to perform the transformation. This method will retain the data source for as long as the subscription isn't disposed (result `Disposable` being disposed). In case `source` observable sequence terminates successfully, the data source will present latest element until the subscription isn't disposed. - parameter dataSource: Data source used to transform elements to view cells. - parameter source: Observable sequence of items. - returns: Disposable object that can be used to unbind. */ public func items< DataSource: RxTableViewDataSourceType & UITableViewDataSource, O: ObservableType> (dataSource: DataSource) -> (_ source: O) -> Disposable where DataSource.Element == O.E { return { source in // This is called for sideeffects only, and to make sure delegate proxy is in place when // data source is being bound. // This is needed because theoretically the data source subscription itself might // call `self.rx.delegate`. If that happens, it might cause weird side effects since // setting data source will set delegate, and UITableView might get into a weird state. // Therefore it's better to set delegate proxy first, just to be sure. _ = self.delegate // Strong reference is needed because data source is in use until result subscription is disposed return source.subscribeProxyDataSource(ofObject: self.base, dataSource: dataSource as UITableViewDataSource, retainDataSource: true) { [weak tableView = self.base] (_: RxTableViewDataSourceProxy, event) -> Void in guard let tableView = tableView else { return } dataSource.tableView(tableView, observedEvent: event) } } } } extension Reactive where Base: UITableView { /** Reactive wrapper for `dataSource`. For more information take a look at `DelegateProxyType` protocol documentation. */ public var dataSource: DelegateProxy { return RxTableViewDataSourceProxy.proxy(for: base) } /** Installs data source as forwarding delegate on `rx.dataSource`. Data source won't be retained. It enables using normal delegate mechanism with reactive delegate mechanism. - parameter dataSource: Data source object. - returns: Disposable object that can be used to unbind the data source. */ public func setDataSource(_ dataSource: UITableViewDataSource) -> Disposable { return RxTableViewDataSourceProxy.installForwardDelegate(dataSource, retainDelegate: false, onProxyForObject: self.base) } // events /** Reactive wrapper for `delegate` message `tableView:didSelectRowAtIndexPath:`. */ public var itemSelected: ControlEvent { let source = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didSelectRowAt:))) .map { a in return try castOrThrow(IndexPath.self, a[1]) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:didDeselectRowAtIndexPath:`. */ public var itemDeselected: ControlEvent { let source = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didDeselectRowAt:))) .map { a in return try castOrThrow(IndexPath.self, a[1]) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:accessoryButtonTappedForRowWithIndexPath:`. */ public var itemAccessoryButtonTapped: ControlEvent { let source: Observable = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:accessoryButtonTappedForRowWith:))) .map { a in return try castOrThrow(IndexPath.self, a[1]) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:commitEditingStyle:forRowAtIndexPath:`. */ public var itemInserted: ControlEvent { let source = self.dataSource.methodInvoked(#selector(UITableViewDataSource.tableView(_:commit:forRowAt:))) .filter { a in return UITableViewCellEditingStyle(rawValue: (try castOrThrow(NSNumber.self, a[1])).intValue) == .insert } .map { a in return (try castOrThrow(IndexPath.self, a[2])) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:commitEditingStyle:forRowAtIndexPath:`. */ public var itemDeleted: ControlEvent { let source = self.dataSource.methodInvoked(#selector(UITableViewDataSource.tableView(_:commit:forRowAt:))) .filter { a in return UITableViewCellEditingStyle(rawValue: (try castOrThrow(NSNumber.self, a[1])).intValue) == .delete } .map { a in return try castOrThrow(IndexPath.self, a[2]) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:moveRowAtIndexPath:toIndexPath:`. */ public var itemMoved: ControlEvent { let source: Observable = self.dataSource.methodInvoked(#selector(UITableViewDataSource.tableView(_:moveRowAt:to:))) .map { a in return (try castOrThrow(IndexPath.self, a[1]), try castOrThrow(IndexPath.self, a[2])) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:willDisplayCell:forRowAtIndexPath:`. */ public var willDisplayCell: ControlEvent { let source: Observable = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:))) .map { a in return (try castOrThrow(UITableViewCell.self, a[1]), try castOrThrow(IndexPath.self, a[2])) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:didEndDisplayingCell:forRowAtIndexPath:`. */ public var didEndDisplayingCell: ControlEvent { let source: Observable = self.delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didEndDisplaying:forRowAt:))) .map { a in return (try castOrThrow(UITableViewCell.self, a[1]), try castOrThrow(IndexPath.self, a[2])) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:didSelectRowAtIndexPath:`. It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, or any other data source conforming to `SectionedViewDataSourceType` protocol. ``` tableView.rx.modelSelected(MyModel.self) .map { ... ``` */ public func modelSelected(_ modelType: T.Type) -> ControlEvent { let source: Observable = self.itemSelected.flatMap { [weak view = self.base as UITableView] indexPath -> Observable in guard let view = view else { return Observable.empty() } return Observable.just(try view.rx.model(at: indexPath)) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:didDeselectRowAtIndexPath:`. It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, or any other data source conforming to `SectionedViewDataSourceType` protocol. ``` tableView.rx.modelDeselected(MyModel.self) .map { ... ``` */ public func modelDeselected(_ modelType: T.Type) -> ControlEvent { let source: Observable = self.itemDeselected.flatMap { [weak view = self.base as UITableView] indexPath -> Observable in guard let view = view else { return Observable.empty() } return Observable.just(try view.rx.model(at: indexPath)) } return ControlEvent(events: source) } /** Reactive wrapper for `delegate` message `tableView:commitEditingStyle:forRowAtIndexPath:`. It can be only used when one of the `rx.itemsWith*` methods is used to bind observable sequence, or any other data source conforming to `SectionedViewDataSourceType` protocol. ``` tableView.rx.modelDeleted(MyModel.self) .map { ... ``` */ public func modelDeleted(_ modelType: T.Type) -> ControlEvent { let source: Observable = self.itemDeleted.flatMap { [weak view = self.base as UITableView] indexPath -> Observable in guard let view = view else { return Observable.empty() } return Observable.just(try view.rx.model(at: indexPath)) } return ControlEvent(events: source) } /** Synchronous helper method for retrieving a model at indexPath through a reactive data source. */ public func model(at indexPath: IndexPath) throws -> T { let dataSource: SectionedViewDataSourceType = castOrFatalError(self.dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx.items*` methods was used.") let element = try dataSource.model(at: indexPath) return castOrFatalError(element) } } @available(iOS 10.0, tvOS 10.0, *) extension Reactive where Base: UITableView { /// Reactive wrapper for `prefetchDataSource`. /// /// For more information take a look at `DelegateProxyType` protocol documentation. public var prefetchDataSource: DelegateProxy { return RxTableViewDataSourcePrefetchingProxy.proxy(for: base) } /** Installs prefetch data source as forwarding delegate on `rx.prefetchDataSource`. Prefetch data source won't be retained. It enables using normal delegate mechanism with reactive delegate mechanism. - parameter prefetchDataSource: Prefetch data source object. - returns: Disposable object that can be used to unbind the data source. */ public func setPrefetchDataSource(_ prefetchDataSource: UITableViewDataSourcePrefetching) -> Disposable { return RxTableViewDataSourcePrefetchingProxy.installForwardDelegate(prefetchDataSource, retainDelegate: false, onProxyForObject: self.base) } /// Reactive wrapper for `prefetchDataSource` message `tableView(_:prefetchRowsAt:)`. public var prefetchRows: ControlEvent<[IndexPath]> { let source = RxTableViewDataSourcePrefetchingProxy.proxy(for: base).prefetchRowsPublishSubject return ControlEvent(events: source) } /// Reactive wrapper for `prefetchDataSource` message `tableView(_:cancelPrefetchingForRowsAt:)`. public var cancelPrefetchingForRows: ControlEvent<[IndexPath]> { let source = prefetchDataSource.methodInvoked(#selector(UITableViewDataSourcePrefetching.tableView(_:cancelPrefetchingForRowsAt:))) .map { a in return try castOrThrow(Array.self, a[1]) } return ControlEvent(events: source) } } #endif #if os(tvOS) extension Reactive where Base: UITableView { /** Reactive wrapper for `delegate` message `tableView:didUpdateFocusInContext:withAnimationCoordinator:`. */ public var didUpdateFocusInContextWithAnimationCoordinator: ControlEvent<(context: UITableViewFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator)> { let source = delegate.methodInvoked(#selector(UITableViewDelegate.tableView(_:didUpdateFocusIn:with:))) .map { a -> (context: UITableViewFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator) in let context = try castOrThrow(UITableViewFocusUpdateContext.self, a[1]) let animationCoordinator = try castOrThrow(UIFocusAnimationCoordinator.self, a[2]) return (context: context, animationCoordinator: animationCoordinator) } return ControlEvent(events: source) } } #endif