Tips-1. 优雅的注册可复用的表格视图
疯狂的热身运动 协议 WsReusable
包含一个只读的属性 identifier
,这个属性返回的是一个遵循该协议的类的类名的字符串儿,有点儿绕口,但不难理解 😶
swift
public protocol WsReusable: class {
static var identifier: String { get }
}
extension WsReusable {
public static var identifier: String {
// 这里的 describing 可以变成 reflecting ,reflecting
// 更加完整的表述了类名
return String(describing: Self.self)
}
}
只需让我们自定义的 UICollectionViewCell
遵循该协议,他就免费获得了下面的注册方式:
swift
extension Ws where Base: UICollectionView {
// 封装了一层 UICollectionViewCell的注册方法,自动把 cell 的 identifier
// 属性作为 ReuseIdentifier
public func register<T: UICollectionViewCell>(cell: T.Type) where T: WsReusable {
base.register(cell, forCellWithReuseIdentifier: T.identifier)
}
}
然后又添加了一个复用方法,复用的时候也不需要强转cell类型了
swift
extension Ws where Base: UICollectionView {
.../
public func dequeueReusableCell<T: UICollectionViewCell>(for indexPath: IndexPath) -> T where T: WsReusable {
// 这里把cell类型解包
guard let cell = base.dequeueReusableCell(withReuseIdentifier: T.identifier, for: indexPath) as? T else {
fatalError("Could not dequeue reusable cell with identifier: \(T.identifier)")
}
return cell
}
}
当然,获得这些方法的前提还是你的 UICollectionViewCell
遵循 WsReusable
协议哦~
直奔主题
swift
// 第一步,遵守协议
class FamilyManagerCell: UICollectionViewCell, WsReusable { ... }
// 第二步,注册
collectionView.ws.register(cell: FamilyManagerCell.self)
// 第三步,复用
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// 无需强转
let cell: FamilyManagerCell = collectionView.ws.dequeueReusableCell(for: indexPath)
return cell
}
高潮冲刺 不仅是 UICollectionViewCell
,UITableViewCell
也可以这么用 甚至继承自 UICollectionReusableView
类型的 UICollectionView
的 sectionHeader
和 sectionFooter
也可以遵守 WsReusable
协议,只不过注册时候的写法稍稍有所改变,篇幅限制,不再赘述。
贤者模式 粗暴的字符串赋值就像潘多拉魔盒,是bug之源,而swift的协议+泛型真的可以把一切粗暴的东西变的优雅和赏心悦目。
Tips-2 命名空间由 rx_ 到 rx.
你是否有给自己封装的方法或属性加过前缀呢?在 OC
时代推荐的是 前缀_
的方式,而在 Swift
时代,由于其语言的特性,面向协议编程,我们接触到了 .rx
.kf
这样比较优雅的方式,于是在万顺的 WSUIKit
组件库里就有了这个东西 WS.swift.
实现原理,代码如下
swift
public struct Ws<Base> {
public let base: Base
init(_ base: Base) {
self.base = base
}
}
// 凡是遵守了 WsProtocol 协议的类型就都获取了 ws 属性
// ws 的类型是 Ws, Ws的关联类型(泛型)就是他自己
public protocol WsProtocol {}
extension WsProtocol {
public var ws: Ws<Self> {
return Ws(self)
}
public static var ws: Ws<Self>.Type {
return Ws.self
}
}
// 所有 NSObject 的子类都默认遵循了 WsProtocol
extension NSObject: WsProtocol {}
使用
swift
extension Ws where Base: UICollectionView {
public func register<T: UICollectionViewCell>(cell: T.Type) where T: WsReusable {
base.register(cell, forCellWithReuseIdentifier: T.identifier)
}
}
collectionView.ws.register(cell: FamilyManagerCell.self)
优雅,永不过时
Tips-3 将 MJRefresh 用的更 RxSwift 一点
在主工程的 home
目录下有一个 MJRefresh+ws.swift
目的
-
监听
MJRefreshComponent
的刷新状态,将这个状态封装成一个 ControlEvent,我们只订阅"正在刷新中(refreshing)"的状态。 -
将各种刷新动作抽象成一个
MJRefreshAction
,将header和footer的各种类似beginRefreshing
这样的动作封装起来,便于解耦viewModel
和viewController
之间的粘连代码,避免在viewModel
中出现类似header.beginRefreshing
的代码,将mvvm的架构变得更加纯洁😈。 -
MJRefresh+ws
还贴心的为UIScrollView
增加了addHeader
方法,你可以直接调swifttableVie.ws.addHeader()
这样的代码,而不必去初始化一个
MJRefreshNormalHeader
或者WSRefreshGifHeader
。如果某天我们的产品或者设计突然开窍了,觉得现在的下拉刷新菊花效果太丑了,要设计一个带动效的样式,那我们可以毫不犹豫的把代码改成这样swifttableView.ws.addHeader(animation: true)
可能其他端两天的工作量,我们只需要两分钟!这属于是提前预判了产品需求~
MJRefresh+ws 正确使用五步走
1、添加 header/footer
swift
tableView.ws.addHeader()
2、在 vc 中设置事件
swift
if let header = tableView.mj_header {
header.rx.refresh.subscribe(onNext: { [weak self] in
guard let self = self else { return }
self.viewModel.requestFetchBalanceList(checkMore: false)
}).disposed(by: bag)
}
3、在 viewModel 中创建一个属性,统一处理Refresh的各种状态
swift
let refreshAction = PublishRelay<MJRefreshAction>()
4、refreshAction 的状态变更,在请求结束的回调里执行
swift
self.refreshAction.accept(.stopRefresh)
5、在 vc 中把 refreshAction 绑定到 scrollView 上
swift
viewModel.refreshAction
.asSignal()
.emit(to: tableView.rx.refreshAction)
.disposed(by: bag)
注意事项⚠️:
showNomoreData 状态设置后,一定要记住在合适的时机重置。设置为 resetNomoreData
参照
go
使用场景可以参照 `OtherPaidListController`
设置-亲情号管理-亲情号代付订单列表页
提醒: 如果需要具体代码,
关注后私信
,看到会发你代码。
Tips-4 疯狂打call的 ActivityIndicator(loading显示器)
如何使用?
swift
// viewModel 中的触发
class AddFamilyMemberViewModel {
var loadable: Driver<Bool>
init() {
// 1.初始化一个 ActivityIndicator
let activity = ActivityIndicator()
// 2.loadable 供外界订阅
loadable = activity.asDriver()
dataSource = service.getFamilyType()
// 3.在网络请求的方法后调用trackActivity
.trackActivity(activity)
.map({ users in
...
})
.asDriver(onErrorJustReturn: [])
}
}
// viewController 中的订阅
class AddFamilyMemberController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 4.vc中订阅loadable,即是整个异步请求函数的 loading 过程
viewModel.loadable
.drive(rx.isHUDLoadingDisplay)
.disposed(by: bag)
}
}
实现原理? ActivityIndicator
利用 using 操作符,捕获了前一个 Observable
的生命周期(注意这个生命周期是一个信号开始被订阅到 onNext
或 onError
闭包被执行。这里需要深入理解RxSwift 信号流转的全过程)。也就是说 activity
的关联类型是一个默认为 false
的 Bool
值,当前一个 Observable
开始发射信号的时候,activity
的关联值是 true
, 当前一个Observable
发射的信号被监听到以后,activity
的关联值变回了 false
。从而有效的监听到了一个网络请求的全过程,loadable
顺其自然的绑定到了一个HUD上面。
番外废话: ActivityIndicator
是 RxSwift
作者Krunoslav Zaher
给出来的解决方案,并不是我瞎编的~想学习源码的 github.com/ReactiveX/R...
Tips-5 站在阴暗里的英雄 ErrorIndicator
得益于对 ActivityIndicator
的充分理解,我觉得 ViewModel
中的网络请求错误处理同样可以用这种思路解决。
先看用法
swift
// viewModel
class AddFamilyMemberViewModel: AddFamilyMemberViewModelDataType {
var networkError: Driver<NetworkingError>
init () {
// 网络请求中可能出现的错误捕捉
let error = ErrorIndicator()
networkError = error.compactMap { ($0 as! NetworkingError) }
.asDriver()
dataSource = service.getFamilyType()
.trackError(error)
.map({ users in
...
})
.asDriver(onErrorJustReturn: [])
}
}
// viewController
class AddFamilyMemberController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 捕捉网络请求的错误,提示用户
viewModel.networkError.drive(onNext: { error in
HUD.showMessage("\(error.description)")
}).disposed(by: bag)
}
}
这和ActivityIndicator
的用法基本没两样😱,甚至 ErrorIndicator
的实现代码更加简单😱
swift
public class ErrorIndicator: SharedSequenceConvertibleType {
public typealias Element = Swift.Error?
public typealias SharingStrategy = DriverSharingStrategy
private let _lock = NSRecursiveLock()
private let _relay = PublishRelay<Element>()
private let _error: SharedSequence<SharingStrategy, Element>
public init() {
_error = _relay.asDriver(onErrorJustReturn: nil)
}
fileprivate func trackErrorOfObservable<Source: ObservableConvertibleType>(
_ source: Source,
justReture element: Source.Element?) -> Observable<Source.Element> {
// 这里只需要使用 catchError 这个操作符把 error 捕捉到,回传给
// ErrorIndicator 即可
return source.asObservable().catchError { error in
self._lock.lock()
self._relay.accept(error)
self._lock.unlock()
if let e = element {
return Observable<Source.Element>.just(e)
} else {
return Observable<Source.Element>.empty()
}
}
}
public func asSharedSequence() -> SharedSequence<DriverSharingStrategy, Element> {
return _error
}
}
extension ObservableConvertibleType {
public func trackError(_ errorIndicator: ErrorIndicator,
justReturn element: Element? = nil)
-> Observable<Element> {
return errorIndicator.trackErrorOfObservable(self, justReture: element)
}
}
这叫抄吗?这叫学以致用~
后续
之后我还会更新我在实践项目里是如何设计 ViewModel 的心得,以及一些参考代码,关注我,一起学习~
Swift 实践小技巧持续更新中~