This is the 29th day of my participation in the Gwen Challenge.More article challenges

venti

The plan did not change quickly, so the code and text of this article are close to 4200+, which is beyond my expectation, which is the super cup of the whole series, haha.

As always, let’s start with the UI

First of all, I want to make the following summary for the project, the public number and the system:

  • The project and the public account page structure is the same, the system just changed a Tabs page to a UITableView. The interfaces are different, the models are the same.

  • When the Tab is switched, if it is the first time that the Tab is cut, then the data will be pulled down to refresh. If it is not the first time that the Tab is switched, then the previous data will be displayed.

  • The implementation and logic for the list page is the same as for the home page.

  • The key logical service is the Tabs switch, which ensures that the list request of the corresponding topic is made when each Tab is switched. In the system page, click a single Cell to open a different topic page.

Then the emphasis comes to the compilation and use of Tabs.

I miss Flutter when I see this Tab feature, so how do I write Flutter in Swift?

Flutter is especially comfortable to write because it natively supports Tab headers:

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: All (0.0), indicatorPadding: EdgeInsets. All (0.0), indicatorWeight: 2.3, unselectedLabelColor: Colors.white, ); }Copy the code

However, in Swift, I had to find a special wheel to do this, so I sacrificed Swift’s wheel, JXSegmentedView.

This wheel actually has an OC version of JXCategoryView, JXSegmentedView is rewritten via Swift. Of course there is more protocol-oriented programming.

Since I was using JXCategoryView at OC, I used its Swift version directly this time as well.

Write interfaces and ViewModel layers

API and service writing

  • API:
{static let tags = "Project /tree/json" static let tagList =" Project /list/" Enum PublicNumber {static let tags = "wxarticle/ Chapters /json" static let tagList {static let tags = "Tree /json" static let tags =" Tree /json" static let tagList = "article/list/" } }Copy the code
  • Service:

The project 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
    }
}

Copy the code

Public account 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
    }
}
Copy the code

System 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
    }
}
Copy the code

As you can see, the interfaces of these three services are very similar to those of pass-throughs. At one point, I even wanted to put these apis together, as well as the Service, but at the time of writing this article, it was too late to change the code and write the article, so I will keep it that way and continue to optimize it.

  • The Model:

The interface data model for the above three service requests is consistent as follows:

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?

}
Copy the code

It is worth noting that there is no value in children in a single Tab of project and public account, while children in a single Tab of system has a value.

  • Enum:

Since the service is very reusable, we write an enumeration to distinguish the project, public account, system business, and its title and the start of the page, we distinguish:

import Foundation enum TagType { case project case publicNumber case tree } extension TagType { var title: String {switch self {case. project: return "project" case. publicNumber: return "publicNumber" case. tree: Var pageNum: Int {switch self {case. project: return 1 case. publicNumber: return 1 case. tree: return 0 } } }Copy the code
  • The ViewModel:

Project, public number, system page sharing the same ViewModel, through the initialization of the incoming different types, used to carry out different business requests, considering the system of business and project, public number slightly different, So I used an alias typeAlias TreeViewModel = TabsViewModel to redefine it.

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() {inputs func loadData() {inputs func loadData() {inputs func loadData() {inputs func loadData()}} 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: Request (TreeService. Tags).map(BaseModel<[Tab]>.self)} result. map{$0.data} // remove nil .compactMap{ $0 } .subscribe(onSuccess: { items in self.dataSource.accept(items) }) .disposed(by: disposeBag) } }Copy the code

After receiving the loadData input, the ViewModel makes different service requests for different services and outputs the dataSource of different services.

Project, official account page preparation

Because the project and the public account of the page is exactly the same, so first talk about these two pages, everyone please pay attention to the notes oh:

Import UIKit import JXSegmentedView class TabsController: BaseViewController {/// initialize the incoming page type. TagType private lazy var segmentedDataSource: JXSegmentedTitleDataSource = JXSegmentedTitleDataSource () / / / lazy loading Tabs private lazy var segmentedView: JXSegmentedView = JXSegmentedView() private var tagSelectRefreshIndexs Set<Int> = [] var contentScrollView: UIScrollView! Var listVCArray = [SingleTabListController]() 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() Private func setupUI() {// set title = type.title // SegmentedViewDataSource must be strongly held by property, Simple configuration segmentedDataSource. IsTitleColorGradientEnabled = true segmentedDataSource. TitleSelectedColor =. SystemBlue SegmentedView. The dataSource = segmentedDataSource / / / configuration indicator let indicator = JXSegmentedIndicatorLineView () indicator.indicatorWidth = JXSegmentedViewAutomaticDimension indicator.lineStyle = .lengthen indicator.indicatorColor = .SystemBlue segmentedView. Indicators = [indicator] /// Configure JXSegmentedView attributes, Delegate = self View. addSubview(segmentedView) /// Initialize contentScrollView contentScrollView = UIScrollView() contentScrollView.isPagingEnabled = true contentScrollView.showsVerticalScrollIndicator = false contentScrollView.showsHorizontalScrollIndicator = false contentScrollView.scrollsToTop = false ContentScrollView. Bounces = false / / / disable automaticallyInset contentScrollView. ContentInsetAdjustmentBehavior =. Never / / / Container control is added to the view on the addSubview (contentScrollView) / / / will contentScrollView and segmentedView contentScrollView correlation SegmentedView. ContentScrollView = contentScrollView / / / layout segmentedView segmentedView. SNP. MakeConstraints {make in make.top.equalTo(view).offset(kTopMargin) make.leading.trailing.equalTo(view) make.height.equalTo(44) } /// Layout contentScrollView contentScrollView. SNP. MakeConstraints {make in the make. Top. EqualTo segmentedView. SNP. (bottom) Make. Leading. Trailing. EqualTo (view) make. Bottom. EqualTo (view). Offset (- kBottomMargin)} / / / network request requestData ()}} Extension TabsController {func requestData() {// create ViewModel let ViewModel = TabsViewModel(type: type, disposeBag: Rx.disposebag) /// request viewModel.loadData() /// get [Tabs] data and drive segmentedView and contentScrollView viewModel.dataSource.asDriver().drive { [weak self] tabs in self?.settingSegmentedDataSource(tabs: tabs) }.disposed(by: rx.disposeBag) } func settingSegmentedDataSource(tabs: [Tab]) {/// [Tabs] enter [String], And refresh data segmentedDataSource. Titles = tabs. The map {$0. Name?. ReplaceHtmlElement}. CompactMap {$0} SegmentedView. DefaultSelectedIndex = 0 segmentedView. ReloadData () / / / remove SingleTabListController view of child controls for vc in ListVCArray {vc. View. RemoveFromSuperview ()} / / / empty array listVCArray removeAll () / / / create SingleTabListController through (Tabs) _ = tabs.map {TAB in /// Note that there is a callback for SingleTabListController to click the cell to call back its model, push the page,  let vc = SingleTabListController(type: type, tab: tab) { webLoadInfo in self.pushToWebViewController(webLoadInfo: WebLoadInfo)} / / / add vc's view to the contentScrollView contentScrollView. AddSubview (vc) view) will be added to listVCArray created by vc ListVCArray. Append (vc)} / / size/configuration contentScrollView contentSize contentScrollView contentSize = CGSize (width: contentScrollView.bounds.size.width * CGFloat(segmentedDataSource.dataSource.count), height: ContentScrollView. Bounds. The size, height) / / / configure each vc. The view of the 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. The size, height)} / / / only to the first SingleTabListController listVCArray to request, If let firstVC = listvcarray.first {firstvc. requestData(isFirstVC: True) /// Mark the page to be refreshed to avoid switching back and forth between tag requests, Affect the user experience tagSelectRefreshIndexs. Insert (0)} / / / the view layout the setNeedsLayout ()}} / / / JXSegmentedViewDelegate proxy method extension TabsController: JXSegmentedViewDelegate {/// TAB switch on segmentedView, Clicking TAB or sliding contentScrollView will cause the TAB to change func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) {/ / / if the refresh before, direct return if tagSelectRefreshIndexs. The contains (index) {return} / / / if there is no refresh, On this page to request listVCArray [index] requestData () tagSelectRefreshIndexs. Insert (index)}}Copy the code

System page writing

The Tab page has a large number of Tabs, children in each Tab, and a large number of Tabs in each Tab. This is typically a UITableView with sections.

RxSwift also encapsulates methods for UITableView with section, which can be used directly, although I think it’s a bit complicated.

import UIKit import RxSwift import RxCocoa import NSObject_Rx import RxDataSources import SnapKit /// 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 / / / get indexPath tableView. Rx. ItemSelected. Bind {[weak self] (indexPath) in the self? .tableView.deselectRow(at: indexPath, animated: false) print(indexPath) } .disposed(by: Rx. DisposeBag) / / / get the cell model of the tableView. Rx. ModelSelected (Tab. The self). The 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. The loadData () / / / bind data viewModel. The dataSource. The subscribe (onNext: { [weak self] tabs in self?.tableViewSectionAndCellConfig(tabs: tabs) }) .disposed(by: Rx. DisposeBag) / / / rewrite emptyDataSetButtonTap. Subscribe {_ in the viewModel. Inputs. The loadData ()}. The disposed (by: rx.disposeBag) } private func tableViewSectionAndCellConfig(tabs: [Tab]) {guard tabs. Count > 0 else {return} /// 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}}) / / set the head title dataSource. TitleForHeaderInSection = {ds, Index in return ds.sectionModels[index].model.name} // Bind (to: tableView.rx.items(dataSource: dataSource)) .disposed(by: rx.disposeBag) } }Copy the code

Preparation of SingleTabListController in project, official account and system page

In the above code, our contentScrollView is used to load the list page, and this page is the project, public account, system page are reused:

  • 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: Private Extension SingleTabListViewModel {func refresh() {loadMore() {loadMore()}}} 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 (" request: \ (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, Map (BaseModel< page <Info>>.self)} result $0.data instead of $0.data.datas. map{$0.data} // Subscribe {event in /// subscribe event in /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// / Through the page on the value judgment is a drop-down or pull (can use enumeration), regardless of success or failure is to end the refresh status, self. PageNum = = self. The pageNum? Self. RefreshSubject. OnNext (. StopRefresh) : self.refreshSubject.onNext(.stopLoadmore) switch event { case .success(let pageModel): Datas = pagemodel. datas {if self.pagenum == if self.pagenum == if self.pagenum == if self.pagenum == if self.pagenum == if self.pagenum == Self.type. pageNum {self.datasource. Accept (datas)}else {self.datasource Self.datasource. Accept (self.datasource. Value + datas)}} Let pageCount = pagemodel. pageCount {// if they are found to be equal, it is the last one, change the foot and state if curPage == pageCount { Self. RefreshSubject. OnNext (. ShowNomoreData)}} case. The error (_) : / / / the error of not do processing 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 } }Copy the code
  • 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 // get indexPath tableView.rx.itemSelected .bind { [weak self] (indexPath) in self?.tableView.deselectRow(at: indexPath, animated: (false)}. Disposed by: rx. DisposeBag) / / / get the cell model of tableView. Rx. ModelSelected (Info. Self). The subscribe (onNext: { [weak self] model in guard let self = self else { return } if self.type == .tree { self.pushToWebViewController(webLoadInfo: Model)}else {/// Nested pages can't be pushed, callback to main controller Push self.cellSelected?(model)} print(" Model is :\(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: /// bind data 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) / / / pull-down and pull-up state is bound to the 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() } } }Copy the code

Written before the SingleTabListViewModel SingleTabListController and RxSwiftCoinRankListController very similar, I personally before you refer to the thinking of writing.

Note this method:

func requestData(isFirstVC: Bool = false) { if isFirstVC { tableView.contentInset = UIEdgeInsets(top: -54, left: 0, bottom: 0, right: 0) } tableView.mj_header? .beginRefreshing() }Copy the code

It is used to actively call the interface for data requests, this is due to Tab switching and is the first time the Tab is switched to the active call.

Update Subject in SingleTabListViewModel: BehaviorSubject

= BehaviorSubject(value: StopRefresh) is not actively refreshed. The value of refreshSubject is changed by the requestData of the controller. If the value is not offset up, the Mj_header will be displayed on the page, resulting in a UI exception.

  • If the tableView.contentinset in the first VC had not been offset up by 54, we would have seen something like this:

Code:

Effect:

ContentInset is offset 54 from the tableView.contentInset of the first VC. If the tableview. contentInset of the first VC is offset 54 from the tableview. contentInset of the first VC is offset 54 from the tableView.contentInset of the first VC is offset 54 from the tableView.

Code:

Effect:

The reason for the 54 is by looking at the height of the mj_header layer:

conclusion

By now, the construction of project, public account and system page has been basically completed.

The homepage, project, official account, system, login and registration pages of wanAndroid client have been basically analyzed, and the analysis of my page and login status has also been written.

This article has also become my largest amount of text and code, mainly because I think these pages are too similar, breaking up the explanation will cut off the previous connection.

In fact, if several pages are done independently, it will feel very simple, but after observing this simplicity for a long time, you will realize the need to encapsulate and pull away.

As for the network request processing mode of multiple pages in contentScrollView, only when I experience the memory explosion caused by multiple requests at one time, I will realize that I cannot handle the network request triggered by page life cycle in the previous way.

Tab back and forth, not every switch must be pulled down refresh, this affects the experience, through the way of marking to deal with.

The configuration of TableView.contentinSet in SingleTabListController was debugged after several attempts.

All the above problems can only be found, solved and improved after project practice.

The project address

RxStudy

Make sure you go to the Play_Android branch

More text and code is not easy, please give a Star ~

Tomorrow to continue

Tomorrow will be the last day of daily update in June. The main page of wanAndroid client has been basically finished.

Tomorrow will continue to explain some small points and project review summary.

Come on, everybody!