CleanUtils
这个工具包的需求源于在所有项目中重复使用相同的基类和扩展来实现 Clean Architecture 以及 viewModels 和 dataStates 的清洁使用。
CleanUtils 主要设计用于包含在大多数项目中,因此类在扩展中留下空间进行定制。
示例
示例项目主要是为了开发目的,实际上并没有真正展示项目的实际功能
仍然,要运行示例项目,克隆仓库,然后首先从 Example 目录运行 pod install
。
需求
- RxSwift
- RxCocoa
安装
CleanUtils 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile:
pod 'CleanUtils'
文档
在本节中,您可以找到一个类列表以及它们在示例中的使用方法。
状态
所有状态都包含一个基本函数 .relay()
和可选项,指定哪些源可以被检索 ...
其中一些是模板化类型,其他只是初始化为纯文本,包含一个 Any?
对象。
它们以这种方式在 viewModel 中初始化。它产生一个 BehaviorRelay<State>
。
行动状态
行动状态是为了执行一个动作,并在其间可选地获取一些数据而设计的。
有关更多信息,请参见基本示例。
可用变量
public let response: Any? // Response object
public let loading: Bool // Sounds obvious ^^
public let error: Error? // Error object if one has happened
数据状态
数据状态是一种允许加载单个对象的状态。它还允许从远程源和本地源同时加载。使用您选择的本地区本数据库,调用 loadRemote()
和 loadLocal()
并使用从数据源获得的相应 Observable
。
它包含一些有用的函数来绑定加载和成功事件,CleanUITableView 和 CleanUICollectionView 将稍后解释。
有关更多信息,请参见基本示例。
可用变量
public let data: Data? // Data being a templated type you stated at init
public let localEnabled: Bool // Have you enabled a local source at init ?
public let localLoading: Bool // Is your local source still loading the data ?
public let remoteEnabled: Bool // Have you enabled a remote source at init ?
public let remoteLoading: Bool // Is your remote source still loading the data ?
public let refreshLoading: Bool // Used for refreshing the whole struct
public let error: Error? // Has any error happened during the fetch ? (that's the last happened)
集合状态
集合状态基本上等同于数据状态,但使用了对象的数组。
这里的关键是,集合状态被设计用来处理分页。
有关更多信息,请参见分页示例。
所有变量在DataState中都被包含在CollectionState中。增加的变量
public let paginationEnabled: Bool // Pagination can be switched off if not used
public let paginationLoading: Bool // Is a new page being loaded ?
public let currentPage: Int
public let totalPages: Int // can usually be fetched from the server reponse headers
public let totalItems: Int // can usually be fetched from the server reponse headers
分页
分页化
该对象需要3种类型的数据
public let data: T?
public let itemsCount: Int? // Total count of items server-side
public let pagesCount: Int? // Number of pages available to request
它从您的网络返回初始化,以下是一个示例
let data = try Mapper<T>(context: context).mapArray(JSONString: jsonString)
let totalString = response.allHeaderFields["pagination-total-count"] as? String ?? "0"
let total = Int(totalString) ?? 0
return Paged(data: data, itemsCount: total, pagesCount: total / perPage + 1)
Paged
的实际实现取决于您的webServices以及数据如何返回。
PagedCollectionController
PagedCollectionController是管理您页面处理的数据结构:剩余、加载、数据...
初始化时需要一个闭包作为参数:(_ page: Int) -> Observable<Paged<[T]>>
,其中T
是您请求的对象类型。
通常,这个闭包将是一个网络调用,使用请求数据中的页码来加载相应的页面。
有关更多信息,请参见分页示例。
PagedUITableView & PagedUICollectionView
是UITableView和UICollectionView的子类,用于与PagedCollectionController一起工作并处理分页。
它们在视图顶部添加了pullToRefresh,因为从iOS10开始,UITableView原生支持。
可以在初始化后设置loadingColor
属性,以设置refreshControl的颜色。
它们可以使用经典的Delegate & DataSource,但如果您想要它们分页,还会提供PagedUITableViewDataSource & PagedUICollectionViewDataSource。
这些协议与UIKit的相应协议相同,它们只是在该协议中添加了两个函数。
public protocol PagedUITableView: UITableView {
func loadMore() // Called when a new page needs to be loaded
func refreshData() // Used to reload the whole tableView on pullToRefresh
}
注意:当UI达到表/collectionView的最后5个条目时,将触发loadMore()
。因此,在实例化单元格时,请记得将其在tableView中注册,并作为可重用的单元格进行队列管理,否则分页将无法工作。
它们的工作方式基本上是一样的。
有关更多信息,请参见分页示例。
干净的ViewModel
这是所有ViewModel应继承的基ViewModel类。
它包含了一些简单但有用的元素,例如
- 输入/输出管理
- DisposeBag (RxSwift)
这个类是如何工作的,以及如何声明它……好吧,让我们直接来看实例,实际上非常简单!
基本示例
假设你有一个ProfileViewController,你可以在其中添加用户为好友,声明一个CleanViewModel看起来是这样的
enum ProfileInput {
case addUserTapped
}
enum ProfileOutput {
case userAdded
}
class ProfileViewModel: CleanViewModel<ProfileInput, ProfileOutput> {
let userInteractor: UserInteractor // Clean Architecture :)
let userState = DataState<User>.relay(remoteEnabled: true)
let addState = ActionState.relay(remoteEnabled: true)
var user: User? {
return userState.value.data as? User
}
override init() {
userInteractor = UserInteractor() // Clean Architecture :)
super.init()
// Load process works with loadRemote & loadLocal, applies to all kinds of State
// Here launched on init, but can also be triggered in the perform, doesn't matter
userInteractor
.fetchUser() // This is basically a RxSwift.Observable
.loadRemote(with: userState) // This loads data into the state
.disposed(by: self)
}
override func perform(_ input: ProfileInput) {
switch input {
case .addUserTapped:
// Load process with only success handled, works only for ActionState
// this one's disposal is handled in background
//
// You can also use the executeAction with only addState as parameter and handle both
// success and error by subscribing to addState in your ViewController.
userInteractor
.addUser() // Again, a RxSwift.Observable
.executeAction(with: addState,
viewModel: self,
success: .userAdded)
}
}
}
然后,在viewController的一侧,你只需要做的是
class ProfileViewController: UIViewController {
@IBOutlet weak var addButton: UIButton!
let viewModel = ProfileViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.userState
.subscribe(onNext: { [unowned self] state in
if state.isGlobalLoading {
return // Data is still loading
}
if let user = viewModel.user {
self.updateView(forUser: user) // Data was fetched and mapped successfully
} else {
// Could not fetch data
}
})
.disposed(by: self.disposeBag)
viewModel
.subscribe(onOutput: { output in
switch output {
case .userAdded:
print("User has been added successfully")
}
})
.disposed(by: self.disposeBag)
addButton
.rx.tap.mapTo(ProfileInput.addUserTapped)
.bind(to: self.viewModel)
.disposed(by: self.disposeBag)
}
}
分页示例
假设你有一个MessagesViewController,你需要显示消息的分页列表,声明CleanViewModel看起来是这样的enum MessagesInput {
case refreshData
case loadMore
}
enum MessagesOutput {
}
class MessagesViewModel: CleanViewModel<MessagesInput, MessagesOutput> {
let messagesInteractor: MessagesInteractor // Clean Architecture :)
lazy var pagedController = PagedCollectionController<Message> { [unowned self] page in
return self.messagesInteractor.getMessages(forPage: page)
}
var messagesState: BehaviorRelay<CollectionState<Message>> {
return pagedController.relay
}
var messages: [Message] {
return messagesState.value.data ?? []
}
override init() {
messagesInteractor = MessagesInteractor() // Clean Architecture :)
super.init()
}
override func perform(_ input: ProfileInput) {
switch input {
case .loadMore:
pagedController.loadMore()
case .refreshData:
pagedController.refreshData()
}
}
}
然后,在viewController的一侧
class MessagesViewController: UIViewController {
@IBOutlet weak var tableView: PagedUITableView!
let viewModel = MessagesViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
self.viewModel
.messagesState
.bindToPagedTableView(self.tableView)
.disposed(by: self.disposeBag)
self.viewModel.perform(.refreshData)
}
}
extension ProfileViewController: PagedUITableViewDataSource {
// don't forget the required function to conform to UITableViewDataSource
// such as for example:
func numberOfSections(in tableView: UITableView) {
return viewModel.messages
}
// and add the two extras from PagedUITableViewDataSource
func loadMore() {
self.viewModel.perform(.loadMore)
}
func refreshData() {
self.viewModel.perform(.refreshData)
}
}
这样就设置好了,每当tableView/collectionView滚动到最后一项的5项之前,它就会触发下一页的请求。
作者
'Tonbouy', Nico Ribeiro, [email protected]
特别感谢'ndmt', Nicolas Dumont,没有他就不会有这一切。
许可证
CleanUtils可在MIT许可证下获得。有关更多详情,请参阅LICENSE文件。