Tips-1. 优雅的注册可复用的表格视图
疯狂的热身运动
协议 WsReusable 包含一个只读的属性 identifier,这个属性返回的是一个遵循该协议的类的类名的字符串儿,有点儿绕口,但不难理解 😶
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 遵循该协议,他就免费获得了下面的注册方式:
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类型了
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 协议哦~
直奔主题
// 第一步,遵守协议
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.
实现原理,代码如下
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 {}
使用
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方法,你可以直接调
这样的代码,而不必去初始化一个tableVie.ws.addHeader()MJRefreshNormalHeader或者WSRefreshGifHeader。如果某天我们的产品或者设计突然开窍了,觉得现在的下拉刷新菊花效果太丑了,要设计一个带动效的样式,那我们可以毫不犹豫的把代码改成这样可能其他端两天的工作量,我们只需要两分钟!这属于是提前预判了产品需求~tableView.ws.addHeader(animation: true)
MJRefresh+ws 正确使用五步走
1、添加 header/footer
tableView.ws.addHeader()
2、在 vc 中设置事件
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的各种状态
let refreshAction = PublishRelay<MJRefreshAction>()
4、refreshAction 的状态变更,在请求结束的回调里执行
self.refreshAction.accept(.stopRefresh)
5、在 vc 中把 refreshAction 绑定到 scrollView 上
viewModel.refreshAction
.asSignal()
.emit(to: tableView.rx.refreshAction)
.disposed(by: bag)
注意事项⚠️:
showNomoreData 状态设置后,一定要记住在合适的时机重置。设置为 resetNomoreData
参照
使用场景可以参照 `OtherPaidListController`
设置-亲情号管理-亲情号代付订单列表页
提醒: 如果需要具体代码,
关注后私信,看到会发你代码。
Tips-4 疯狂打call的 ActivityIndicator(loading显示器)
如何使用?
// 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 中的网络请求错误处理同样可以用这种思路解决。
先看用法
// 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的实现代码更加简单😱
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)
}
}
这叫抄吗?这叫学以致用~
Tips-6 使用属性包装器避免Decodable遇到解析失败的问题
struct Person: Decodable {
var age: Int
var name: String
var score: Int
}
假如我们在使用Decodable把一段json数据 decode 成Person的时候,如果碰到josn里面score字段是nil,而我们按照接口文档的规定,score是不能为nil,这显然是后端接口的问题,但是这会导致我们解析错误,这个时候我们可以利用属性包装器,帮score字段设置一个默认值.
public protocol DefaultValueProvider {
associatedtype Value: Equatable & Codable
static var `default`: Value { get }
}
public enum Zero: DefaultValueProvider {
public static let `default` = 0
}
DefaultValueProvider 协议为我们提供了一系列的默认值类型(后续想要什么类型自己往后新增即可,本例只列举了默认 Int 为0的例子),此时我们的属性包装器设计成如下:
@propertyWrapper
public struct Default<Provider: DefaultValueProvider>: Codable {
public var wrappedValue: Provider.Value
public init() {
wrappedValue = Provider.default
}
public init(wrappedValue: Provider.Value) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
wrappedValue = Provider.default
} else {
wrappedValue = try container.decode(Provider.Value.self)
}
}
}
extension Default: Equatable where Provider.Value: Equatable {}
extension Default: Hashable where Provider.Value: Hashable {}
重写了 Codable 协议的初始化方法,decode 出当前字段为 nil 的时候,wrappedValue 设置成默认值即可。大功告成,使用方式
// 定义结构体
struct Person1: Decodable {
var age: Int
var name: String
@Default<Zero>
var score: Int
}
// JSON 数据字符串
let jsonString = """
{
"age": 25,
"name": "John Doe",
"score": null
}
"""
if let jsonData = jsonString.data(using: .utf8) {
do {
let person = try JSONDecoder().decode(Person1.self, from: jsonData)
print("Age: \(person.age), Name: \(person.name), Score: \(person.score)")
// 此时发现,成功打印 Score: 0
} catch {
print("解码失败: \(error)")
}
} else {
print("转换 JSON 字符串为 Data 失败")
}
后续对DefaultValueProvider的拓展:
public enum False: DefaultValueProvider {
public static let `default` = false
}
public enum True: DefaultValueProvider {
public static let `default` = true
}
public enum Nil<A>: DefaultValueProvider where A: Codable, A: Equatable {
public static var `default`: A? { nil }
}
public enum Empty<A>: DefaultValueProvider where A: Codable, A: Equatable, A: RangeReplaceableCollection {
public static var `default`: A { A() }
}
public enum EmptyDictionary<K, V>: DefaultValueProvider where K: Hashable & Codable, V: Equatable & Codable {
public static var `default`: [K: V] { Dictionary() }
}
public enum Zero: DefaultValueProvider {
public static let `default` = 0
}
public enum ZeroDouble: DefaultValueProvider {
public static let `default`: Double = 0
}
后续
之后我还会更新我在实践项目里是如何设计 ViewModel 的心得,以及一些参考代码,关注我,一起学习~
Swift 实践小技巧持续更新中~