Swift 开发 wanandroid 客户端——分别使用Swift与RxSwift构建积分排行榜页面

1,614 阅读6分钟

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

终于要正面刚RxSwift了。

编写Swift和RxSwift的感受

其实我想说的是,Swift和RxSwift根本就是两种语言!

在很长一段时间里,我的内心一直都深深的畏惧着RxSwift,一大推闭包回调,加上各种面向协议实现,各种面向协议的泛型使用,让我稍微看了一下源码就跪了。

这里其实我陷入了一个误区,就是为什么要看源码呢?先上车会用再说!

让我重燃对RxSwift的兴趣,不是Swift,而是Dart中的BLoC思想和Vue。后面有篇年中总结会专门讲我这大半年的心路历程,敬请期待。

Dart中天然的Stream还有Vue的开箱即用的MVVM模式深深的吸引了我。虽然Swift也有Combine框架了,不过所谓编码语言一通百通,学会了RxSwift还怕Combine学不会,这显然不可能。

另外只有领悟了Rx的思想,其他的Rx系列也会同步理解,真是一石多鸟,话不多说,开始今天的撸代码时间吧。

需求:编写积分排行榜页面

这个页面大概长这个样子:

IMG_0439.PNG

非常的简单,一个TableView,网络请求,然后拿到数据源并刷新列表。

在iOS端开发中有这样一个玩笑话:通过网络请求加载数据,并使用TableView展示出来,那么iOS编程就入门了80%。所以说这个页面入门却不简单,而且是使用Swift与RxSwift分别编写。

还是一点点开始吧,先写共同的网络请求。

网络请求模块

因为不管是Swift还是RxSwift都会用同一套Moya封装进行网络请求,所以先写这个模块。

由于前面已经讲过玩安卓App的网络请求模块编写了,这里我就一笔带过,直接上代码:

  • Api中进行静态变量编写
struct Api {
    /// baseUrl
    static let baseUrl = "https://www.wanandroid.com/"
    
    private init() {}
    
    /// 我的
    enum My {
        static let coinRank = "coin/rank/"
        
        /// 这个接口就是我昨天翻车的那个接口,需要登录后在请求头中进行配置才能请求成功
        static let userCoinInfo = "lg/coin/userinfo/json"
    }
}

  • 编写MyService

import Moya

enum MyService {
    case coinRank(_ page: Int)
    case userCoinInfo
}

extension MyService: TargetType {
    var baseURL: URL {
        return URL(string: Api.baseUrl)!
    }
    
    var path: String {
        switch self {
        case .coinRank(let page):
            return Api.My.coinRank + page.toString + "/json"
        case .userCoinInfo:
            return Api.My.userCoinInfo
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .coinRank, .userCoinInfo:
            return .get
                            
        }
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case .coinRank, .userCoinInfo:
            return .requestParameters(parameters: Dictionary.empty, encoding: URLEncoding.default)
        }
    }
    
    var headers: [String : String]? {
        /// 这里写的是我自定义header,只是为了请求coinRank,return nil即可
        return loginHeader
    }
}
  • 初始化myProvider:
let myProvider: MoyaProvider<MyService> = {
        let stubClosure = { (target: MyService) -> StubBehavior in
            return .never
        }
        return MoyaProvider<MyService>(stubClosure: stubClosure, plugins: [RequestLoadingPlugin()])
}()
  • 模型编写

其实昨天的MyCoin一模一样,是可以复用的,只是为了区别一下,一个是个人的,一个是排名的,所以还是独立写了的。

另外BaseModel与Page,这里就不多做解释。

struct CoinRank : Codable {

    let coinCount : Int?
    let level : Int?
    let nickname : String?
    let rank : String?
    let userId : Int?
    let username : String?
}

struct Page<Content: Codable> : Codable {
    let curPage : Int?
    let datas : [Content]?
    let offset : Int?
    let over : Bool?
    let pageCount : Int?
    let size : Int?
    let total : Int?
}

struct BaseModel<T: Codable>: Codable {
    let data : T?
    let errorCode : Int?
    let errorMsg : String?
}

Swift编写页面

import UIKit

import Moya

class SwiftCoinRankListController: BaseViewController {

    private lazy var tableView = UITableView(frame: .zero, style: .plain)
    
    private var dataSource: [CoinRank] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        getCoinRank()
    }
    
    private func setupTableView() {
        
        /// 注册cell
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        
        /// 设置tableFooterView
        tableView.tableFooterView = UIView()
        
        /// 设置数据源与代理
        tableView.dataSource = self
        tableView.delegate = self
        
        /// 简单布局
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(view)
        }
    }

}

extension SwiftCoinRankListController {
    /// 这里只获取第一页的数据
    private func getCoinRank() {
        myProvider.request(MyService.coinRank(1)) { (result: Result<Response, MoyaError>)  in
            switch result {
                case .success(let response):
                    let data = response.data
                    guard let baseModel = try? JSONDecoder().decode(BaseModel<Page<CoinRank>>.self, from: data), let array = baseModel.data?.datas else {
                        return
                    }
                    self.dataSource = array
                    self.tableView.reloadData()
                    
                case .failure(let error):
                    print(error.errorDescription)
            }
        }
    }
}

extension SwiftCoinRankListController: UITableViewDataSource {
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let coinRank = dataSource[indexPath.row]
        
        if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") {
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        }else {
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        }
    }
}

extension SwiftCoinRankListController: UITableViewDelegate {}

这段代码可以说是非常典型的Cocoa代码,编写UI,设置tableView的数据源与代理,然后在进行网络请求,在回调结果中的.success枚举中,拿到response中的data,data通过JSONDecoder转指定的model,model塞给dataSource,tableView刷新,页面刷新。

好了,我们再看看RxSwift是怎么写的。

RxSwift编写页面

import UIKit

import RxSwift
import RxCocoa
import Moya

class RxSwiftCoinRankListController: BaseViewController {

    lazy var tableView = UITableView(frame: .zero, style: .plain)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    private func setupTableView() {
        
        /// 设置tableFooterView
        tableView.tableFooterView = UIView()
        
        /// 设置代理
        tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
        
        /// 简单布局
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(view)
        }
        
        /// 这里只获取第一页的数据
        myProvider.rx.request(MyService.coinRank(1))
            .map(BaseModel<Page<CoinRank>>.self)
            .map{ $0.data?.datas }
            .compactMap { $0 }
            .asDriver(onErrorJustReturn: [])
            .drive(tableView.rx.items) { (tableView, row, coinRank) in
            if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") {
                cell.textLabel?.text = coinRank.username
                cell.detailTextLabel?.text = coinRank.coinCount?.toString
                return cell
            }else {
                let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
                cell.textLabel?.text = coinRank.username
                cell.detailTextLabel?.text = coinRank.coinCount?.toString
                return cell
            }
        }
        .disposed(by: rx.disposeBag)
    }

}

extension RxSwiftCoinRankListController: UITableViewDelegate {}

虽然在目前这个页面上,使用Swift和RxSwift的代码行数只相差30行,但是随着业务逻辑的增加,RxSwift的整体体量会少很多。

这里必须吧这段核心代码拿出来说一下,请看注释:

/// myProvider.rx.request是Moya的Rx写法,会返回一个Single<Response>,Single是一个专门为网络请求提供的特化序列,它要么只能发出一个元素,要么产生一个error事件,昨天的文章中也有提到。
myProvider.rx.request(MyService.coinRank(1))
    /// 第一个map相当于Swift中Moya回调中的Response中的data转指定的model
    .map(BaseModel<Page<CoinRank>>.self)
    /// 第二个map是获取model.data.datas数据,注意此时获取的datas还是可选类型
    .map{ $0.data?.datas }
    /// compactMap将过滤掉可选类型,简而言之这三步就是Swift原生中.success中的guard部分
    .compactMap { $0 }
    /// 将Single序列转为Driver序列,如果转换错误就返回空数组,至于Driver也是一种特化序列,它主要是为了简化 UI 层的代码。不过如果你遇到的序列具有以下特征,你也可以使用它:不会产生 error 事件,一定在 MainScheduler 监听(主线程监听),共享附加作用
    .asDriver(onErrorJustReturn: [])
    /// 驱动tableView中的数据源码
    .drive(tableView.rx.items) { (tableView, row, coinRank) in
    /// cell的赋值预算,其实还可以封装一次,是针对cell的赋值,这里为了简化就没有写了
        if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") {
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        }else {
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        }
    }
    .disposed(by: rx.disposeBag)

要一口气看明白上面这段代码并不是容易的事情,我自己写个页面的时候,差不多边看RxSwift的文档边写代码和调试花了2天时间。

其实Rx对于新手最为困难就是如何理解序列这个概念,如果不是有其他语言的经历,我估计也难以消化。

总结

RxSwift通过响应式函数式编程大大简化了Cocoa分散式的编程语言方式,将代码的整体风格提升一个档次。

同时,RxSwift对于不熟悉业务与代码的人造成了极大的挑战,阅读困难,调试困难。

之所以说RxSwift是另外一门语言,因为首先你需要有Swift的函数式编程基础与思路,另外需要对于序列的理解。这些都是学习成本。

而RxSwift的源码庞大到根本就不想阅读,截图整整三页(这只是RxSwift,还有其他辅助框架): image.png image.png image.png

好了,我也不是想吓唬大家,反正我打算运用好这个框架,理解其核心思想,至于源码就让它见鬼去吧。

明日继续

今天用Swift于RxSwift分别写了积分排行榜页面,但是其实这个页面的核心功能我们还没有写:下拉刷新与上拉加载。

后面的工作我们会围绕RxSwift对这个页面进一步编写,通过Swift的如何写这个页面就不再演示了。

另外还有基类控制器BaseViewController、BaseTableViewController的编写也会逐一说明,明日继续。

大家加油!