先看下最终的效果吧:
. | . |
---|---|
) |
当然这里也有一段完整的视频链接 预览视频
MVVM
网上已经有很多资料了,作为第一次实际使用的我不做过的的解释,请各位自行查阅相关文章。这里金记录下我的使用感受,ViweModel
很方便复用,代码分层很清楚。RxSwift
本人也是前几天从0开始学习的,也不会给出过多的介绍。如果您也想自己实现一次点这里我已经帮你做好了一份基本的框架,当然完整版的也有,在文章最底部。(已经添加了 SwiftLint
)
先来看看完成后的 ViewController
的核心实际代码:
class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! let disposeBag = DisposeBag() let viewModel = XTRecommendViemModel() override func viewDidLoad() { super.viewDidLoad() initialUI() bindViewModel() viewModel.viewDidload() } } // MARK: initial UI extension ViewController { private func initialUI() { view.backgroundColor = .white title = "数据请求" initialTableView() } private func initialTableView() { // 预估行高 会造成 cell 的重复创建和销毁 例如 本来应该创建 6个, // 预估行高会创建 7 到 8 个然后在你下划或者上划到头后就开始销毁多余的, 再次滑动又会创建新的 // 测试机为 6, iOS12.4 和 XR iOS13.5 tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 260 } } // MARK: iniatal rx extension ViewController { func bindViewModel() { // 配置下拉刷新 let tableHeader = MJRefreshNormalHeader() tableHeader.isAutomaticallyChangeAlpha = true tableHeader.lastUpdatedTimeLabel?.isHidden = true tableView.mj_header = tableHeader tableView.mj_footer = MJRefreshBackNormalFooter() // 绑定数据 tableHeader.rx.refreshing .asDriver() .drive(onNext: { [weak self] _ in self?.viewModel.loadData(true) }) .disposed(by: disposeBag) tableView.mj_footer?.rx.refreshing .asDriver() .drive(onNext: { [weak self] _ in self?.viewModel.loadData(false) }) .disposed(by: disposeBag) // 刷新状态管理 viewModel.refreshStatusBind(to: self.tableView).disposed(by: disposeBag) let dataSource = self.tableViewCellDataSourc // 获取数据 self.viewModel.outputs.dataSource .drive(tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) } } /// 配置 tableViewCell 的数据源 extension ViewController { var tableViewCellDataSourc: RxTableViewSectionedReloadDataSource<SectionModel<String, UserActivity>> { let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, UserActivity>>( configureCell: { [weak self] (_, tabView, indexPath, model) -> UITableViewCell in let sCell = tabView.dequeueReusableCell(withIdentifier: "XTCell", for: indexPath) guard let cell = sCell as? XTCell else { return sCell} cell.confirgueCell(model: model) if !cell.hasBindStream { guard let self = self else { return cell } cell.updaBindState() let imageTapObserver: Binder<ImageViewTapInfo> = Binder(self, scheduler: MainScheduler.instance) { viewController, info in viewController.showPhotoBrowser(info: info) } cell.bindImageTapStream(observer: imageTapObserver) } return cell }) return dataSource } } // MARK: 显示相册 private extension ViewController { func showPhotoBrowser(info: ImageViewTapInfo) { ... self.showXTPhotoBrowser(from: info.sourceView, imagesUrl: orgImageUrlArray, selsctIndex: selectIndex) } func showXTPhotoBrowser(from sourceView: UIImageView, imagesUrl: [String], selsctIndex: Int) { guard !imagesUrl.isEmpty else { return } let orgImageUrlArray = imagesUrl let imageView = sourceView let browser = JXPhotoBrowser() ... browser.show() } } 复制代码
实际的代码行数为 160
行,具体的可以在完整版中看到
作为 ViewModel
的 XTRecommendViemModel
中的核心代码
/** * * Inputs 只提供方法 * Outputs 提供 Observable * */ protocol XTRecommendViemModelInputs { /// 加载数据 /// - Parameter isRefreshing: 是否为刷新,**true**就加入到头部,**false**加入尾部 func loadData(_ isRefreshing: Bool) /// 界面已经加载 func viewDidload() } protocol XTRecommendViemModelOutPuts { /// 数据源数组 var dataSource: Driver<[SectionModel<String, UserActivity>]> { get } } protocol XTRecommendViemModelType { var inputs: XTRecommendViemModelInputs { get } var outputs: XTRecommendViemModelOutPuts { get } } class XTRecommendViemModel: XTRecommendViemModelType, XTRecommendViemModelInputs, XTRecommendViemModelOutPuts { let refreshStauts = BehaviorRelay<RefreshStatus>(value: .none) // 协议 var inputs: XTRecommendViemModelInputs { return self } var outputs: XTRecommendViemModelOutPuts { return self } // inputs func loadData(_ isRefreshing: Bool) { // 结束上次的刷新状态 refreshStauts.accept(isRefreshing ? .endFooterRefresh : .endHeaderRefresh) loadDataProperty.onNext(isRefreshing) } func viewDidload() { refreshStauts.accept(.hiddendFooter) refreshStauts.accept(.begainHeaderRefresh) } // outputs var dataSource: Driver<[SectionModel<String, UserActivity>]> { let loadNewCommand = loadDataProperty .filter { $0 } .asDriver { _ -> Driver<Bool> in return Driver.just(true) } .flatMap { [weak self] _ -> Driver<[UserActivity]> in guard let `self` = self else { return Driver<[UserActivity]>.just([]) } return self.queryNewData() } .flatMap { [weak self] items -> Driver<XTRecommendViemModel.EditeDataCommand> in self?.refreshStauts.accept(.endHeaderRefresh) self?.refreshStauts.accept(.showFooter) return Driver.just(EditeDataCommand.loadNewData(items: items)) } let loadMoreCommand = loadDataProperty .filter { !$0 } .asDriver { _ in Driver.just(true) } .flatMap { [weak self] _ -> Driver<[UserActivity]> in guard let `self` = self else { return Driver<[UserActivity]>.just([]) } return self.queryNextPageData() } .flatMap { [weak self] items -> Driver<XTRecommendViemModel.EditeDataCommand> in // 判断 items 和 noNext 字段 // 选择 hiddendFooter, endFooterRefresh, endFooterRefreshWithNoData self?.refreshStauts.accept(.endFooterRefresh) return Driver.just(EditeDataCommand.loadOldData(items: items)) } let initialWrappedModel = RecommendWrappedModel() let dataSource = Driver.of(loadNewCommand, loadMoreCommand) .merge() .scan(initialWrappedModel) { (resultWrapped: RecommendWrappedModel, command: EditeDataCommand) -> RecommendWrappedModel in return resultWrapped.execute(command: command) } .map { wrappedModel -> [SectionModel<String, UserActivity>] in return [SectionModel(model: "XTRemSectionModel", items: wrappedModel.items)] } return dataSource } // private private let loadDataProperty: PublishSubject<Bool> = PublishSubject() // 网络请求 //private lazy var recommendProvider = { MoyaProvider<JueJinGraphqlAPI>() }() } /// 这里就是刷新状态控制的协议 extension XTRecommendViemModel: Refreshable {} // MARK: 真实网路请求 private extension XTRecommendViemModel { func queryNewData() -> Driver<[UserActivity]> { /* 网络请求被本地数据替换,公开所爬接口是不道德的 */ let items = readerLoadData() let result = Driver<[UserActivity]>.just(items).delay(.milliseconds(1500)) return result } func queryNextPageData() -> Driver<[UserActivity]> { let items = readerLoadData() let result = Driver<[UserActivity]>.just(items).delay(.milliseconds(1500)) return result } } // MARK: 生成指定形式的数据 private extension XTRecommendViemModel { // 加载本地数据 func readerLoadData() -> [UserActivity] { let model = UserActivity.modelFromLocal() let result = model.shuffled().suffix(10) return Array(result) } } // MARK: 定义数据的转换方式 private extension XTRecommendViemModel { enum EditeDataCommand { /// instert 到头部 case loadNewData(items: [UserActivity]) /// append 到尾部 case loadOldData(items: [UserActivity]) } /// 内部数据存储的数据结构 struct RecommendWrappedModel { fileprivate var items: [UserActivity] init(items: [UserActivity] = []) { self.items = items } /// 核心 func execute(command: EditeDataCommand) -> RecommendWrappedModel { switch command { case let .loadNewData(insertItems): return RecommendWrappedModel(items: insertItems) case let .loadOldData(appedItems): var tmpArray = self.items tmpArray.append(contentsOf: appedItems) return RecommendWrappedModel(items: tmpArray) } } } // The end } 复制代码
真实行数为 170
带有注释
由于是第一次用,我这注释应该还算详实吧😅。
Input
协议指定了 VC
能够调用的方法,Output
则是在内部处理数据后向 VC
提供数据。然后定义了一个新的协议 XTRecommendViemModelType
内部需要遵守协议者提供 inputs
和 outputs
属性, 再让 XTRecommendViemModel
同时遵守着三个协议,其 inputs
和 outputs
都返回 self
class XTRecommendViemModel: XTRecommendViemModelType, XTRecommendViemModelInputs, XTRecommendViemModelOutPuts { ... // 协议 var inputs: XTRecommendViemModelInputs { return self } var outputs: XTRecommendViemModelOutPuts { return self } } 复制代码
这样外部在使用时需要向 ViewModel
传递信息就使用 viewModel.inputs.xxx
, 需要 ViewModel
提供信息就通过 viewModel.outputs.xxx
,分离职责,代码逻辑分层。
Inputs
中的
func loadData(_ isRefreshing: Bool) 复制代码
对应的是 XTRecommendViemModel
中的
private let loadDataProperty: PublishSubject<Bool> = PublishSubject() 复制代码
VC
每次调用 loadData(:)
就会使 loadDataProperty
发送一次 stream
, XTRecommendViemModel
的 dataSource
就通过 flatMap
这个 PublishSubject<Bool>
来生相的。这里先按下不表,让我们来看看 XTRecommendViemModel
中真正处理和存储数据的类型 struct RecommendWrappedModel
和 enum EditeDataCommand
;
EditeDataCommand
定义了对数据操作的类型,我在这里仅仅定义了刷新数据的 loadNew
和添加数据的 loadMore
,实际上是可以根据需求拓展出 reloadIndex
等。
enum EditeDataCommand { /// instert 到头部 case loadNewData(items: [UserActivity]) /// append 到尾部 case loadOldData(items: [UserActivity]) } 复制代码
RecommendWrappedModel
内部的数组 private var items: [UserActivity]
就是数据存储的位置, 其对外部只提供 func execute(command: EditeDataCommand) -> RecommendWrappedModel {...}
方法用于操作数据
/// 核心 func execute(command: EditeDataCommand) -> RecommendWrappedModel { switch command { case let .loadNewData(insertItems): return RecommendWrappedModel(items: insertItems) case let .loadOldData(appedItems): var tmpArray = self.items tmpArray.append(contentsOf: appedItems) return RecommendWrappedModel(items: tmpArray) } } 复制代码
现在来看看 dataSoure
是如何生成的。
先看 loadNewCommand
let loadNewCommand = loadDataProperty .filter { $0 } // true 进入下一步, false 过滤 .asDriver { _ -> Driver<Bool> in // 当成一次 driver 信号 return Driver.just(true) } .flatMap { [weak self] _ -> Driver<[UserActivity]> in // 内部请求网络数据(异步) guard let `self` = self else { return Driver<[UserActivity]>.just([]) } return self.queryNewData() } .flatMap { [weak self] items -> Driver<XTRecommendViemModel.EditeDataCommand> in // 根据数据的结果发送 刷新状态 self?.refreshStauts.accept(.endHeaderRefresh) self?.refreshStauts.accept(.showFooter) // 将结果转换为 command 命令 return Driver.just(EditeDataCommand.loadNewData(items: items)) } 复制代码
同样的 loadMoreCommand
,
let loadMoreCommand = loadDataProperty .filter { !$0 } // 为 false 表示加载更多 .asDriver { _ in Driver.just(true) } // 内部发送网络请求(异步) .flatMap { [weak self] _ -> Driver<[UserActivity]> in guard let `self` = self else { return Driver<[UserActivity]>.just([]) } // 这里可以发送当前网络请求的状态 类似于 刷新状态的控制 return self.queryNextPageData() } .flatMap { [weak self] items -> Driver<XTRecommendViemModel.EditeDataCommand> in // 判断 items 和 noNext 字段 // 选择 hiddendFooter, endFooterRefresh, endFooterRefreshWithNoData self?.refreshStauts.accept(.endFooterRefresh) // 转换为 command 消息 return Driver.just(EditeDataCommand.loadOldData(items: items)) } 复制代码
然后是把这两个 command
的 stream
进行合并
先把 loadNew
和 loadMore
通过 merge
操作符进行合并,在通过 scan
操作符将每次产生的每次产生的 command
枚举值与 initialWrappedModel
进行匹配 execute(command:)
迭代 wrappedModel
, 在通过 map
方法把 wrappedModel
中的 items
转换为 SectionModel
let dataSource = Driver.of(loadNewCommand, loadMoreCommand) .merge() .scan(initialWrappedModel) { (resultWrapped: RecommendWrappedModel, command: EditeDataCommand) -> RecommendWrappedModel in return resultWrapped.execute(command: command) } .map { wrappedModel -> [SectionModel<String, UserActivity>] in return [SectionModel(model: "XTRemSectionModel", items: wrappedModel.items)] } return dataSource 复制代码
最终生成了 VC
所需要的数据源.
private var kRxRefreshCommentKey: UInt8 = 0 public extension Reactive where Base: MJRefreshComponent { var refreshing: ControlEvent<Void> { let source: Observable<Void> = lazyInstanceObservable(&kRxRefreshCommentKey) { () -> Observable<()> in Observable.create { [weak control = self.base] observer in if let control = control { control.refreshingBlock = { observer.on(.next(())) } } else { observer.on(.completed) } return Disposables.create() } .takeUntil(self.deallocated) .share(replay: 1) } return ControlEvent(events: source) } private func lazyInstanceObservable<T: AnyObject>(_ key: UnsafeRawPointer, createCachedObservable: () -> T) -> T { if let value = objc_getAssociatedObject(self.base, key) as? T { return value } let observable = createCachedObservable() objc_setAssociatedObject(self.base, key, observable, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return observable } } 复制代码
这里是模仿 RxCocoa
对 UIBarButton
进行的拓展,目的是放置类似于一个 header.rx
被重复订阅,导致每次都生成一个 Observer
使前面的订阅不起效果。
public enum RefreshStatus { case none, begainHeaderRefresh, endHeaderRefresh case hiddendFooter, showFooter, endFooterRefresh, endFooterRefreshWithNoData } public protocol Refreshable { var refreshStauts: BehaviorRelay<RefreshStatus> { get } } public extension Refreshable { func refreshStatusBind(to scrollView: UIScrollView) -> Disposable { return refreshStauts.subscribe(onNext: { [weak scrollV = scrollView] status in switch status { case .none: break case .begainHeaderRefresh: scrollV?.mj_header?.beginRefreshing() case .endHeaderRefresh: scrollV?.mj_header?.endRefreshing() case .hiddendFooter: scrollV?.mj_footer?.isHidden = true case .showFooter: scrollV?.mj_footer?.isHidden = false case .endFooterRefresh: scrollV?.mj_footer?.endRefreshing() case .endFooterRefreshWithNoData: scrollV?.mj_footer?.endRefreshingWithNoMoreData() } }) } } 复制代码
在 XTRecommendViemModelInputs
中作如下修改
protocol XTRecommendViemModelInputs: Refreshable 复制代码
就可以直接在 VC
中通过如下调用
viewModel.inputs.refreshStatusBind(to: self.tableView).disposed(by: disposeBag) 复制代码
来控制 tableView
刷新状态。
类似的还可以添加空数据展示视图等。
本 demo
还有很多可以改进的地方
tableView
的数据源现在存在两份, 一份是 viewModel
中,一份是 RxTableViewReloadDataSource
中,在改的话会将 ViewModel
中混入 View
的管理所以,我在这并没有将 tableView
的 dataSource
设置为 viewModel
;cell
没有对用的 ViewModel
最后给出完整版本的链接,接口是不可能暴露出来的👺,用的是本地数据啦,想要真实数据,自己想办法喽,毕竟这只是一次练手项目😶。
demo