Swift 开发 wanandroid 客户端——项目、公众号、体系页面编写

这是我参与更文挑战的第29天,活动详情查看: 更文挑战

超大杯

计划没有变化快,于是乎这篇文章的代码和文字接近4200+了,出乎了我的意料,算是整个系列的超大杯了,哈哈。

和往常一样,我们先看看UI

RPReplay_Final1624865065.2021-06-28 15_25_49.gif

首先针对项目、公众号、体系我要做下面几个总结:

  • 项目和公众号的页面结构一致,体系只是将一个Tabs页面改为了UITableView。使用的只是接口不同,模型都是相同。

  • Tab切换的时候,如果是第一次切到该标签,那么就进行下拉刷新进行数据请求,如果不是第一次切换,那么就显示之前的数据即可。

  • 关于列表页的实现和逻辑和首页相同。

  • 关键的逻辑业务在于Tabs切换,保证切换到每个Tab时,进行对应主题的列表请求,体系页面是点击单个Cell,进行不同的主题页面。

那么重点来了——Tabs的编写与使用。

看见这个Tab功能我无比怀念Flutter,所以在Swift中该怎么写呢?

在Flutter中因为原生就支持头部的Tab,所以写起来特别舒服:

TabBar tabBar() {
    return TabBar(
      tabs: _dataSource.map((model) {
        return Tab(
          child: Container(
            padding: EdgeInsets.all(10),
            child: Text(model.name),
          ),
        );
      }).toList(),
      controller: _tabController,
      isScrollable: true,
      indicatorColor: Colors.white,
      indicatorSize: TabBarIndicatorSize.tab,
      labelStyle: TextStyle(color: Colors.white, fontSize: 20),
      unselectedLabelStyle: TextStyle(color: Colors.grey, fontSize: 18),
      labelColor: Colors.white,
      labelPadding: EdgeInsets.all(0.0),
      indicatorPadding: EdgeInsets.all(0.0),
      indicatorWeight: 2.3,
      unselectedLabelColor: Colors.white,
    );
  }
复制代码

然而,在Swift中,我不得不找个轮子专门干这个事,于是祭出Swift的轮子——JXSegmentedView

这个轮子其实有OC版本JXCategoryView,JXSegmentedView就是通过Swift重新写了一遍。当然其中更多的使用了面向协议编程的方式。

由于OC时我在使用JXCategoryView,所以这次也直接用了它的Swift版本。

编写接口与ViewModel层

API与服务编写

  • API:
extension Api {
    /// 项目 均是get请求
    enum Project {
        static let tags = "project/tree/json"
        
        static let tagList = "project/list/"
    }
}

extension Api {
    /// 公众号 均是get请求
    enum PublicNumber {
        static let tags = "wxarticle/chapters/json"

        static let tagList = "wxarticle/list/"
    }
}

extension Api {
    /// 体系 均是get请求
    enum Tree {
        
        static let tags = "tree/json"

        static let tagList = "article/list/"
    }
}
复制代码
  • Service:

项目的Service

import Foundation

import Moya

let projectProvider: MoyaProvider<ProjectService> = {
        let stubClosure = { (target: ProjectService) -> StubBehavior in
            return .never
        }
        return MoyaProvider<ProjectService>(stubClosure: stubClosure, plugins: [RequestLoadingPlugin()])
}()

enum ProjectService {
    case tags
    case tagList(_ id: Int, _ page: Int)
}

extension ProjectService: TargetType {
    var baseURL: URL {
        return URL(string: Api.baseUrl)!
    }
    
    var path: String {
        switch self {
        case .tags:
            return Api.Project.tags
        case .tagList(_, let page):
            return Api.Project.tagList + page.toString + "/json"
        }
    }
    
    var method: Moya.Method {
        return .get
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case .tags:
            return .requestParameters(parameters: Dictionary.empty, encoding: URLEncoding.default)
        case .tagList(let id, _):
            return .requestParameters(parameters: ["cid": id.toString], encoding: URLEncoding.default)
        }
        
    }
    
    var headers: [String : String]? {
        return nil
    }
}

复制代码

公众号的Service:

import Foundation

import Moya

let publicNumberProvider: MoyaProvider<PublicNumberService> = {
        let stubClosure = { (target: PublicNumberService) -> StubBehavior in
            return .never
        }
        return MoyaProvider<PublicNumberService>(stubClosure: stubClosure, plugins: [RequestLoadingPlugin()])
}()

enum PublicNumberService {
    case tags
    case tagList(_ id: Int, _ page: Int)
}

extension PublicNumberService: TargetType {
    var baseURL: URL {
        return URL(string: Api.baseUrl)!
    }
    
    var path: String {
        switch self {
        case .tags:
            return Api.PublicNumber.tags
        case .tagList(let id, let page):
            return Api.PublicNumber.tagList + id.toString + "/" + page.toString + "/json"
        }
    }
    
    var method: Moya.Method {
        return .get
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        return .requestParameters(parameters: Dictionary.empty, encoding: URLEncoding.default)
        
    }
    
    var headers: [String : String]? {
        return nil
    }
}
复制代码

体系的Service:

import Foundation

import Moya

let treeProvider: MoyaProvider<TreeService> = {
        let stubClosure = { (target: TreeService) -> StubBehavior in
            return .never
        }
        return MoyaProvider<TreeService>(stubClosure: stubClosure, plugins: [RequestLoadingPlugin()])
}()

enum TreeService {
    case tags
    case tagList(_ id: Int, _ page: Int)
}

extension TreeService: TargetType {
    var baseURL: URL {
        return URL(string: Api.baseUrl)!
    }
    
    var path: String {
        switch self {
        case .tags:
            return Api.Tree.tags
        case .tagList(_, let page):
            return Api.Tree.tagList + page.toString + "/json"
        }
    }
    
    var method: Moya.Method {
        return .get
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case .tags:
            return .requestParameters(parameters: Dictionary.empty, encoding: URLEncoding.default)
        case .tagList(let id, _):
            return .requestParameters(parameters: ["cid": id.toString], encoding: URLEncoding.default)
        }
        
    }
    
    var headers: [String : String]? {
        return nil
    }
}
复制代码

可以看到的是,其实这三个服务的接口形式与传参都很相似,我甚至一度想把这些Api在一起,Service也放在一起,不过在写这篇文章的时候木已成舟,再接着改代码写文章就有点来不及了,所以暂时保持这样,后续继续优化。

  • Model:

上面三个服务请求的接口数据模型都一致,如下所示:

import Foundation

struct Tab : Codable {

    let children : [Tab]?
    let courseId : Int?
    let id : Int?
    let name : String?
    let order : Int?
    let parentChapterId : Int?
    let userControlSetTop : Bool?
    let visible : Int?

}
复制代码

值得注意的是项目与公众号单个Tab中的children中是没有值的而体系单个Tab中的children是有值的。

  • Enum:

由于服务的复用性很大,所以我们写一个枚举用来区分项目、公众号、体系的业务,而且其title还有起始的页面不同,我们都加以区分:

import Foundation

enum TagType {
    case project
    case publicNumber
    case tree
}

extension TagType {
    var title: String {
        switch self {
        case .project:
            return "项目"
        case .publicNumber:
            return "公众号"
        case .tree:
            return "体系"
        }
    }
    
    var pageNum: Int {
        switch self {
        case .project:
            return 1
        case .publicNumber:
            return 1
        case .tree:
            return 0
        }
    }
}
复制代码
  • ViewModel:

项目、公众号、体系页面共用同一ViewModel,通过初始化时传入不同的类型,用来进行不同的业务请求,考虑体系的业务和项目、公众号的略有不同,所以我用了一个别名typealias TreeViewModel = TabsViewModel来重新定义它。

import Foundation

import RxSwift
import RxCocoa
import Moya

typealias TreeViewModel = TabsViewModel

class TabsViewModel: BaseViewModel {
    
    private let type: TagType

    private let disposeBag: DisposeBag
    
    init(type: TagType, disposeBag: DisposeBag) {
        self.type = type
        self.disposeBag = disposeBag
        super.init()
    }
    
    /// outputs    
    let dataSource = BehaviorRelay<[Tab]>(value: [])
    
    /// inputs
    func loadData() {
        requestData()
    }
}

//MARK:- 网络请求
private extension TabsViewModel {
    func requestData() {
        let result: Single<BaseModel<[Tab]>>
        switch type {
        case .project:
            result = projectProvider.rx.request(ProjectService.tags)
                .map(BaseModel<[Tab]>.self)
        case .publicNumber:
            result = publicNumberProvider.rx.request(PublicNumberService.tags)
                .map(BaseModel<[Tab]>.self)
        case .tree:
            result = treeProvider.rx.request(TreeService.tags)
                .map(BaseModel<[Tab]>.self)
        }
        
        result
            .map{ $0.data }
            /// 去掉其中为nil的值
            .compactMap{ $0 }
            .subscribe(onSuccess: { items in
                self.dataSource.accept(items)
            })
        .disposed(by: disposeBag)
    }
}

复制代码

ViewModel接到loadData的输入后,针对不同的业务进行不同的业务请求,输出不同业务的dataSource

项目、公众号页面编写

因为项目和公众号的页面是一模一样的,所以先讲这两个页面,大家请注意看注释喔:

import UIKit

import JXSegmentedView

class TabsController: BaseViewController {
    
    /// 初始化传入页面类型
    private let type: TagType
    
    /// 懒加载 Tabs数据源
    private lazy var segmentedDataSource: JXSegmentedTitleDataSource = JXSegmentedTitleDataSource()
    
    /// 懒加载 Tabs
    private lazy var segmentedView: JXSegmentedView = JXSegmentedView()
    
    /// 存储点击tag导致的刷新
    private var tagSelectRefreshIndexs: Set<Int> = []
    
    /// 可以滑动的容器View,用于展示不同Tab切换的页面
    var contentScrollView: UIScrollView!
    
    /// SingleTabListController单页面数组
    var listVCArray = [SingleTabListController]()
    
    /// 初始化方法
    init(type: TagType) {
        self.type = type
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
}

extension TabsController {
    /// 页面搭建
    private func setupUI() {
        /// 设置标题
        title = type.title
        
        /// segmentedViewDataSource一定要通过属性强持有,简单配置
        segmentedDataSource.isTitleColorGradientEnabled = true
        segmentedDataSource.titleSelectedColor = .systemBlue
        segmentedView.dataSource = segmentedDataSource

        /// 配置指示器
        let indicator = JXSegmentedIndicatorLineView()
        indicator.indicatorWidth = JXSegmentedViewAutomaticDimension
        indicator.lineStyle = .lengthen
        indicator.indicatorColor = .systemBlue
        segmentedView.indicators = [indicator]

        /// 配置JXSegmentedView的属性,设置代理并添加到view上
        segmentedView.delegate = self
        view.addSubview(segmentedView)

        /// 初始化contentScrollView
        contentScrollView = UIScrollView()
        contentScrollView.isPagingEnabled = true
        contentScrollView.showsVerticalScrollIndicator = false
        contentScrollView.showsHorizontalScrollIndicator = false
        contentScrollView.scrollsToTop = false
        contentScrollView.bounces = false

        /// 禁用automaticallyInset
        contentScrollView.contentInsetAdjustmentBehavior = .never
        
        /// 添加容器控制到view上
        view.addSubview(contentScrollView)

        /// 将contentScrollView和segmentedView.contentScrollView进行关联
        segmentedView.contentScrollView = contentScrollView
        
        /// 布局segmentedView
        segmentedView.snp.makeConstraints { make in
            make.top.equalTo(view).offset(kTopMargin)
            make.leading.trailing.equalTo(view)
            make.height.equalTo(44)
        }
        
        /// 布局contentScrollView
        contentScrollView.snp.makeConstraints { make in
            make.top.equalTo(segmentedView.snp.bottom)
            make.leading.trailing.equalTo(view)
            make.bottom.equalTo(view).offset(-kBottomMargin)
        }
        
        /// 进行网络请求
        requestData()
    }
}

extension TabsController {
    func requestData() {
        /// 创建ViewModel
        let viewModel = TabsViewModel(type: type, disposeBag: rx.disposeBag)
        
        /// 进行请求
        viewModel.loadData()
        
        /// 获取[Tabs]数据并驱动segmentedView与contentScrollView
        viewModel.dataSource.asDriver().drive { [weak self] tabs in
            self?.settingSegmentedDataSource(tabs: tabs)
        }.disposed(by: rx.disposeBag)
        
        
    }
    
    func settingSegmentedDataSource(tabs: [Tab]) {
        /// [Tabs]转[String],并刷新数据
        segmentedDataSource.titles = tabs.map{ $0.name?.replaceHtmlElement }.compactMap{ $0 }
        segmentedView.defaultSelectedIndex = 0
        segmentedView.reloadData()
        
        /// 移除SingleTabListController上view上的子控件
        for vc in listVCArray {
            vc.view.removeFromSuperview()
        }
        
        /// 清空数组
        listVCArray.removeAll()
        
        /// 通过[Tabs]创建SingleTabListController
        let _ = tabs.map { tab in
        
            /// 注意这个初始化中会有一个回调,用于SingleTabListController中点击cell回调其模型,在这个页面进行push操作,
            let vc = SingleTabListController(type: type, tab: tab) { webLoadInfo in
                self.pushToWebViewController(webLoadInfo: webLoadInfo)
            }
            
            /// 将vc的view添加到contentScrollView
            contentScrollView.addSubview(vc.view)
            
            将创建的vc添加到listVCArray
            listVCArray.append(vc)
        }
        
        /// 配置contentScrollView的contentSize大小
        contentScrollView.contentSize = CGSize(width: contentScrollView.bounds.size.width * CGFloat(segmentedDataSource.dataSource.count),
                                               height: contentScrollView.bounds.size.height)
        
        /// 配置每个vc.view的frame
        for (index, vc) in listVCArray.enumerated() {
            vc.view.frame = CGRect(x: contentScrollView.bounds.size.width * CGFloat(index),
                                   y: 0,
                                   width: contentScrollView.bounds.size.width,
                                   height: contentScrollView.bounds.size.height)
        }
        
        /// 仅对listVCArray中的第一个SingleTabListController进行请求,并将请求的做标记
        if let firstVC = listVCArray.first {
            /// 对第一个页面进行请求,避免一口气请求导致内存暴增
            firstVC.requestData(isFirstVC: true)
            
            /// 将进行刷新的页面打一个标记,避免来回切换tag不停的请求,影响用户体验
            tagSelectRefreshIndexs.insert(0)
        }
        
        /// view进行layout
        view.setNeedsLayout()
    }
}

/// JXSegmentedViewDelegate代理方法
extension TabsController: JXSegmentedViewDelegate {

    /// segmentedView上的tab切换,点击tab或者滑动contentScrollView都会导致tab变化
    func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) {
        /// 如果之前刷新过,直接返回
        if tagSelectRefreshIndexs.contains(index) {
            return
        }
        
        /// 如果没有刷新过,就对这个页面进行请求
        listVCArray[index].requestData()
        tagSelectRefreshIndexs.insert(index)
    }
}
复制代码

体系页面编写

体系页面本次有很多个大主题[Tabs],而每个Tab中又有children,里面又是很多子主题[Tabs],这典型就是一个带section的UITableView嘛。

而RxSwift,对于这种带有section的UITableView也为我们封装好了方法,直接用即可,虽然我觉得还是有点复杂,哈哈:

import UIKit

import RxSwift
import RxCocoa
import NSObject_Rx
import RxDataSources
import SnapKit

/// 使用tableView配合section即可完成需求
class TreeController: BaseTableViewController {
    
    private let type: TagType
    
    init(type: TagType) {
        self.type = type
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
}

extension TreeController {
    private func setupUI() {
        title = type.title
        
        tableView.mj_header = nil
        tableView.mj_footer = nil
                
        /// 获取indexPath
        tableView.rx.itemSelected
            .bind { [weak self] (indexPath) in
                self?.tableView.deselectRow(at: indexPath, animated: false)
                print(indexPath)
            }
            .disposed(by: rx.disposeBag)
        
        
        /// 获取cell中的模型
        tableView.rx.modelSelected(Tab.self)
            .subscribe(onNext: { [weak self] tab in
                guard let self = self else { return }
                let vc = SingleTabListController(type: self.type, tab: tab)
                self.navigationController?.pushViewController(vc, animated: true)
            })
            .disposed(by: rx.disposeBag)
                
        let viewModel = TreeViewModel(type: type, disposeBag: rx.disposeBag)

        viewModel.inputs.loadData()

        /// 绑定数据
        viewModel.dataSource
            .subscribe(onNext: { [weak self] tabs in
                self?.tableViewSectionAndCellConfig(tabs: tabs)
            })
            .disposed(by: rx.disposeBag)
        
        /// 重写
        emptyDataSetButtonTap.subscribe { _ in
            viewModel.inputs.loadData()
        }.disposed(by: rx.disposeBag)
    }
    
    private func tableViewSectionAndCellConfig(tabs: [Tab]) {
        guard tabs.count > 0 else {
            return
        }
        
        /// 这种带有section的tableView,不能通过一级菜单确定是否有数据,需要将二维数组进行降维打击
        let children = tabs.map { $0.children }.compactMap { $0 }
        let deepChildren = children.flatMap{ $0 }.map { $0.children }.compactMap { $0 }.flatMap { $0 }
        Observable.just(deepChildren).map { $0.count == 0 }.bind(to: isEmpty).disposed(by: rx.disposeBag)
        
        let sectionModels = tabs.map { tab in
            return SectionModel(model: tab, items: tab.children ?? [])
        }

        let items = Observable.just(sectionModels)

        let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<Tab, Tab>>(configureCell: { (ds, tv, indexPath, element) in
            
            if let cell = tv.dequeueReusableCell(withIdentifier: "Cell") {
                cell.textLabel?.text = ds.sectionModels[indexPath.section].model.children?[indexPath.row].name
                cell.textLabel?.font = UIFont.systemFont(ofSize: 15)
                cell.accessoryType = .disclosureIndicator
                return cell
            }else {
                let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
                cell.textLabel?.text = ds.sectionModels[indexPath.section].model.children?[indexPath.row].name
                cell.textLabel?.font = UIFont.systemFont(ofSize: 15)
                cell.accessoryType = .disclosureIndicator
                return cell
            }
            
        })

        //设置分区头标题
        dataSource.titleForHeaderInSection = { ds, index in
            return ds.sectionModels[index].model.name
        }

        //绑定单元格数据
        items.bind(to: tableView.rx.items(dataSource: dataSource))
            .disposed(by: rx.disposeBag)
    }
}
复制代码

项目、公众号、体系页面中SingleTabListController的编写

上面的代码中,我们的contentScrollView用于加载列表页,而这个页面是项目、公众号、体系页面都是复用的:

  • SingleTabListViewModel:
import Foundation

import RxSwift
import RxCocoa
import Moya

class SingleTabListViewModel: BaseViewModel {

    private var pageNum: Int
    
    private let disposeBag: DisposeBag
        
    private let type: TagType
    
    private let tab: Tab
    
    init(type: TagType, tab: Tab, disposeBag: DisposeBag) {
        self.pageNum = type.pageNum
        self.type = type
        self.tab = tab
        self.disposeBag = disposeBag
        super.init()
    }
    
    /// outputs    
    let dataSource = BehaviorRelay<[Info]>(value: [])
    
    let refreshSubject: BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .stopRefresh)
    
    /// inputs
    func loadData(actionType: ScrollViewActionType) {
        switch actionType {
        case .refresh:
            refresh()
        case .loadMore:
            loadMore()
        }
    }

}

//MARK:- 网络请求,普通列表数据
private extension SingleTabListViewModel {
    
    func refresh() {
        resetCurrentPageAndMjFooter()
        requestData(page: pageNum)
    }
  
    
    func loadMore() {
        pageNum = pageNum + 1
        requestData(page: pageNum)
    }
    
    func requestData(page: Int) {
        guard let id = tab.id else {
            return
        }
        let result: Single<BaseModel<Page<Info>>>
        switch type {
        case .project:
            print("请求:\(id)")
            result = projectProvider.rx.request(ProjectService.tagList(id, page))
                .map(BaseModel<Page<Info>>.self)
        case .publicNumber:
            result = publicNumberProvider.rx.request(PublicNumberService.tagList(id, page))
                .map(BaseModel<Page<Info>>.self)
        case .tree:
            result = treeProvider.rx.request(TreeService.tagList(id, page))
                .map(BaseModel<Page<Info>>.self)
        }
        
        result
            /// 由于需要使用Page,所以return到$0.data这一层,而不是$0.data.datas
            .map{ $0.data }
            /// 解包
            .compactMap { $0 }
            /// 转换操作
            .asObservable()
            .asSingle()
            /// 订阅
            .subscribe { event in
                
                /// 订阅事件
                /// 通过page的值判断是下拉还是上拉(可以用枚举),不管成功还是失败都结束刷新状态
                self.pageNum == self.type.pageNum ? self.refreshSubject.onNext(.stopRefresh) : self.refreshSubject.onNext(.stopLoadmore)
                
                switch event {
                case .success(let pageModel):
                    /// 解包数据
                    if let datas = pageModel.datas {
                        /// 通过page的值判断是下拉还是上拉,做数据处理,这里为了方便写注释,没有使用三目运算符
                        if self.pageNum == self.type.pageNum {
                            /// 下拉做赋值运算
                            self.dataSource.accept(datas)
                        }else {
                            /// 上拉做合并运算
                            self.dataSource.accept(self.dataSource.value + datas)
                        }
                    }
                    
                    /// 解包curPage与pageCount
                    if let curPage = pageModel.curPage, let pageCount = pageModel.pageCount  {
                        /// 如果发现它们相等,说明是最后一个,改变foot而状态
                        if curPage == pageCount {
                            self.refreshSubject.onNext(.showNomoreData)
                        }
                    }
                case .error(_):
                    /// error占时不做处理
                    break
                }
            }.disposed(by: disposeBag)
    }
}

private extension SingleTabListViewModel {
    private func resetCurrentPageAndMjFooter() {
        pageNum = type.pageNum
        refreshSubject.onNext(.resetNomoreData)
    }
}

extension Optional: Error {
    static var wrappedError: String?  { return nil }
}

复制代码
  • SingleTabListController:
import UIKit

import RxSwift
import RxCocoa
import NSObject_Rx
import SnapKit
import MJRefresh

class SingleTabListController: BaseTableViewController {
    
    private let type: TagType
    
    private let tab: Tab
    
    var cellSelected: ((WebLoadInfo) -> Void)?
    
    init(type: TagType, tab: Tab, cellSelected: ((WebLoadInfo) -> Void)? = nil) {
        self.type = type
        self.tab = tab
        self.cellSelected = cellSelected
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    func requestData(isFirstVC: Bool = false) {
        if isFirstVC {
            tableView.contentInset = UIEdgeInsets(top: -54, left: 0, bottom: 0, right: 0)
        }
        tableView.mj_header?.beginRefreshing()
    }
}

extension SingleTabListController {
    private func setupUI() {
        
        title = tab.name

        /// 获取indexPath
        tableView.rx.itemSelected
            .bind { [weak self] (indexPath) in
                self?.tableView.deselectRow(at: indexPath, animated: false)
            }
            .disposed(by: rx.disposeBag)
        
        /// 获取cell中的模型
        tableView.rx.modelSelected(Info.self)
            .subscribe(onNext: { [weak self] model in
                guard let self = self else { return }
                if self.type == .tree {
                    self.pushToWebViewController(webLoadInfo: model)
                }else {
                    /// 嵌套页面无法push,回调到主控制器再push
                    self.cellSelected?(model)
                }
                print("模型为:\(model)")
            })
            .disposed(by: rx.disposeBag)
                
        let viewModel = SingleTabListViewModel(type: type, tab: tab, disposeBag: rx.disposeBag)

        tableView.mj_header?.rx.refresh
            .asDriver()
            .drive(onNext: {
                viewModel.loadData(actionType: .refresh)
                
            })
            .disposed(by: rx.disposeBag)

        tableView.mj_footer?.rx.refresh
            .asDriver()
            .drive(onNext: {
                viewModel.loadData(actionType: .loadMore)
                
            })
            .disposed(by: rx.disposeBag)

        /// 绑定数据
        viewModel.dataSource
            .asDriver(onErrorJustReturn: [])
            .drive(tableView.rx.items) { (tableView, row, info) in
                if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? InfoViewCell {
                    cell.info = info
                    return cell
                }else {
                    let cell = InfoViewCell(style: .subtitle, reuseIdentifier: "Cell")
                    cell.info = info
                    return cell
                }
            }
            .disposed(by: rx.disposeBag)
        
        viewModel.dataSource.map { $0.count == 0 }.bind(to: isEmpty).disposed(by: rx.disposeBag)
        
        /// 下拉与上拉状态绑定到tableView
        viewModel.refreshSubject
            .bind(to: tableView.rx.refreshAction)
            .disposed(by: rx.disposeBag)
        
        if type == .tree {
            tableView.contentInset = UIEdgeInsets(top: -54, left: 0, bottom: 0, right: 0)
            tableView.mj_header?.beginRefreshing()
        }
    }
}

复制代码

这个SingleTabListViewModel与SingleTabListController和之前写的RxSwiftCoinRankListController非常相似,我个人大家参照之前的思路编写即可。

值得注意的是这个方法:

func requestData(isFirstVC: Bool = false) {
    if isFirstVC {
        tableView.contentInset = UIEdgeInsets(top: -54, left: 0, bottom: 0, right: 0)
    }
    tableView.mj_header?.beginRefreshing()
}
复制代码

用来主动去调用接口进行数据请求,这个是由于Tab切换且是第一次切换到该Tab时主动调用。

tableView.contentInset需要向上偏移54个,是因为SingleTabListViewModel中的let refreshSubject: BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .stopRefresh)是不主动刷新的,通过控制器的requestData去改变refreshSubject中的值,如果不向上偏移,mj_header会显示在页面上,导致UI异常。

  • 如果第一个VC中的tableView.contentInset没有向上偏移54,我们看到的就是这样的效果:

代码:

image.png

效果:

RPReplay_Final1624927871.2021-06-29 08_53_28.gif

至于为何仅仅对第一个请求的VC的tableView.contentInset做向上偏移54,因为就我编码观察,如果每一VC的tableView.contentInset都向上偏移54,除了第一个VC显示正常外,其他的VC都显示异常:

代码:

image.png

效果:

RPReplay_Final1624928391.2021-06-29 09_00_49.gif

至于为何是54,是通过看图层观察mj_header的高度得出的:

image.png

总结

到此,项目、公众号、体系页面构建基本完成。

wanandroid客户端的首页、项目、公众号、体系、登录、注册页面都基本上分析完了,涉及我的页面与登录状态的分析也都编写完成。

这一篇也成了我文字和代码量最大的文章,主要是我觉得这几个页面太相似了,拆开讲解反而会切断之前的联系。

而几个页面其实如果独立一个个的做会感觉非常的简单,但是这种简单在观察久了之后,你会意识到需要封装与抽离。

而对于contentScrollView中多个页面的网络请求的处理方式,只有自己体会到一口气多个请求导致内存暴增,才会意识到不能用之前的方式来处理页面生命周期触发网络请求。

Tab来回切换,并不是每一次切换都必须进行下拉刷新,这样影响体验,通过打标记的方式来处理。

SingleTabListController中的tableView.contentInset的配置,是进行多次尝试后才调试好的。

以上这些都只有在项目实践后才能发现问题,解决问题,得到提高。

项目地址

RxStudy

大家记得切到play_android分支上面去喔~

更文与写代码不易,请给一个Star吧~

明日继续

明日就是6月每日更文最后1天,wanandroid客户端的主体页面已经基本说完。

明日会继续讲解一些小的知识点与项目复盘总结。

大家加油!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享