这是我参与更文挑战的第19天,活动详情查看: 更文挑战
终于要正面刚RxSwift了。
编写Swift和RxSwift的感受
其实我想说的是,Swift和RxSwift根本就是两种语言!
在很长一段时间里,我的内心一直都深深的畏惧着RxSwift,一大推闭包回调,加上各种面向协议实现,各种面向协议的泛型使用,让我稍微看了一下源码就跪了。
这里其实我陷入了一个误区,就是为什么要看源码呢?先上车会用再说!
让我重燃对RxSwift的兴趣,不是Swift,而是Dart中的BLoC思想和Vue。后面有篇年中总结会专门讲我这大半年的心路历程,敬请期待。
Dart中天然的Stream还有Vue的开箱即用的MVVM模式深深的吸引了我。虽然Swift也有Combine框架了,不过所谓编码语言一通百通,学会了RxSwift还怕Combine学不会,这显然不可能。
另外只有领悟了Rx的思想,其他的Rx系列也会同步理解,真是一石多鸟,话不多说,开始今天的撸代码时间吧。
需求:编写积分排行榜页面
这个页面大概长这个样子:
非常的简单,一个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,还有其他辅助框架):
好了,我也不是想吓唬大家,反正我打算运用好这个框架,理解其核心思想,至于源码就让它见鬼去吧。
明日继续
今天用Swift于RxSwift分别写了积分排行榜页面,但是其实这个页面的核心功能我们还没有写:下拉刷新与上拉加载。
后面的工作我们会围绕RxSwift对这个页面进一步编写,通过Swift的如何写这个页面就不再演示了。
另外还有基类控制器BaseViewController、BaseTableViewController的编写也会逐一说明,明日继续。
大家加油!