5 个超级实用的Swift日常开发小 Tips

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
}

高潮冲刺 不仅是 UICollectionViewCellUITableViewCell 也可以这么用 甚至继承自 UICollectionReusableView 类型的 UICollectionViewsectionHeadersectionFooter 也可以遵守 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

目的

  1. 监听 MJRefreshComponent 的刷新状态,将这个状态封装成一个 ControlEvent,我们只订阅"正在刷新中(refreshing)"的状态。

  2. 将各种刷新动作抽象成一个 MJRefreshAction,将header和footer的各种类似beginRefreshing 这样的动作封装起来,便于解耦 viewModelviewController 之间的粘连代码,避免在 viewModel中出现类似 header.beginRefreshing 的代码,将mvvm的架构变得更加纯洁😈。

  3. MJRefresh+ws 还贴心的为 UIScrollView 增加了 addHeader方法,你可以直接调

    swift 复制代码
    tableVie.ws.addHeader()

    这样的代码,而不必去初始化一个 MJRefreshNormalHeader 或者 WSRefreshGifHeader。如果某天我们的产品或者设计突然开窍了,觉得现在的下拉刷新菊花效果太丑了,要设计一个带动效的样式,那我们可以毫不犹豫的把代码改成这样

    swift 复制代码
    tableView.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 的生命周期(注意这个生命周期是一个信号开始被订阅到 onNextonError 闭包被执行。这里需要深入理解RxSwift 信号流转的全过程)。也就是说 activity的关联类型是一个默认为 falseBool 值,当前一个 Observable开始发射信号的时候,activity的关联值是 true, 当前一个Observable发射的信号被监听到以后,activity的关联值变回了 false。从而有效的监听到了一个网络请求的全过程,loadable 顺其自然的绑定到了一个HUD上面。

番外废话: ActivityIndicatorRxSwift 作者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 实践小技巧持续更新中~


相关推荐
独木舟的木6 个月前
Combine 会杀死 RxSwift 吗?
swift·rxswift·reactivex
独木舟的木6 个月前
ReactiveCocoa vs RxSwift @kodeco
swift·rxswift
独木舟的木6 个月前
函数响应式编程的基本构建块
swift·rxswift
独木舟的木6 个月前
Backpressure
rxswift·reactivex
独木舟的木6 个月前
为什么我无法描述 FRP,但我却会写
swift·apple·rxswift
独木舟的木6 个月前
RxSwift 编程思想
swift·apple·rxswift
独木舟的木6 个月前
使用 RxSwift 更快地编写代码
swift·apple·rxswift
独木舟的木6 个月前
RxSwift 6 更新了什么
swift·apple·rxswift
独木舟的木6 个月前
observeOn 与 subscribeOn
swift·rxswift
独木舟的木6 个月前
RxSwift 中7个有用的过滤运算符
swift·rxswift