这是我参与更文挑战的第29天,活动详情查看: 更文挑战
超大杯
计划没有变化快,于是乎这篇文章的代码和文字接近4200+了,出乎了我的意料,算是整个系列的超大杯了,哈哈。
和往常一样,我们先看看UI
首先针对项目、公众号、体系我要做下面几个总结:
-
项目和公众号的页面结构一致,体系只是将一个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,我们看到的就是这样的效果:
代码:
效果:
至于为何仅仅对第一个请求的VC的tableView.contentInset做向上偏移54,因为就我编码观察,如果每一VC的tableView.contentInset都向上偏移54,除了第一个VC显示正常外,其他的VC都显示异常:
代码:
效果:
至于为何是54,是通过看图层观察mj_header的高度得出的:
总结
到此,项目、公众号、体系页面构建基本完成。
wanandroid客户端的首页、项目、公众号、体系、登录、注册页面都基本上分析完了,涉及我的页面与登录状态的分析也都编写完成。
这一篇也成了我文字和代码量最大的文章,主要是我觉得这几个页面太相似了,拆开讲解反而会切断之前的联系。
而几个页面其实如果独立一个个的做会感觉非常的简单,但是这种简单在观察久了之后,你会意识到需要封装与抽离。
而对于contentScrollView中多个页面的网络请求的处理方式,只有自己体会到一口气多个请求导致内存暴增,才会意识到不能用之前的方式来处理页面生命周期触发网络请求。
Tab来回切换,并不是每一次切换都必须进行下拉刷新,这样影响体验,通过打标记的方式来处理。
SingleTabListController中的tableView.contentInset的配置,是进行多次尝试后才调试好的。
以上这些都只有在项目实践后才能发现问题,解决问题,得到提高。
项目地址
大家记得切到play_android分支上面去喔~
更文与写代码不易,请给一个Star吧~
明日继续
明日就是6月每日更文最后1天,wanandroid客户端的主体页面已经基本说完。
明日会继续讲解一些小的知识点与项目复盘总结。
大家加油!