先看下最终的效果吧:
. | . |
---|---|
) |
当然这里也有一段完整的视频链接 预览视频
- 关于
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
带有注释
# ViewModel 的解析
由于是第一次用,我这注释应该还算详实吧😅。
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
内部的数组 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
通过 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
所需要的数据源。 viewModel
每次调用 loadData(:)
进行一次 input
操作,就可以在内部转换为一次 output
的 stream
。
# 对 MJRefresh 的 Rx 拓展
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
使前面的订阅不起效果(备注: 不推荐这样实现,这种类似于 target-action
的模式本身就应该只对一个订阅者起效果!)。
# 对刷新状态的控制
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()
}
})
}
}
在 XTRecommendViemModelOutputs
中作如下修改
protocol XTRecommendViemModelOutPuts: Refreshable {
...
}
就可以直接在 VC
中通过如下调用
viewModel.outputs.refreshStatusBind(to: self.tableView).disposed(by: disposeBag)
来控制 tableView
刷新状态。
类似的还可以添加空数据展示视图等。
# 总结
本 demo
还有很多可以改进的地方
tableView
的数据源现在存在两份, 一份是viewModel
中,一份是RxTableViewReloadDataSource
中,在改的话会将ViewModel
中混入View
的管理所以,我在这并没有将tableView
的dataSource
设置为viewModel
;cell
没有对用的ViewModel
最后给出完整版本的链接,接口是不可能暴露出来的👺,用的是本地数据啦,想要真实数据,自己想办法喽,毕竟这只是一次练手项目😶。
最重要的是要附上自己的学习途径,真的对不起呢,现在补充如下: