jsdw_ios/QuickLocation/Main/BaseService/ListService.swift

313 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// ListService.swift
// HealthyZG
//
// Created by on 2020/5/29.
// Copyright © 2020 Lin. All rights reserved.
//
import UIKit
import RxSwift
import Moya
import ObjectMapper
import RxDataSources
import RxSwiftExt
protocol ListServiceType {
associatedtype BaseListModel where BaseListModel: Mappable
associatedtype BaseItem where BaseItem: Mappable
}
typealias PagingTargetTransform = (Int) -> MultiTarget
enum PagingRequestType {
case refresh
case more
case clear
}
typealias RefreshResult = (status: RefreshStatus, isEmpty: Bool)
/// ListType: : ListService<ListModel<PaginationModel>>
class ListService<ListType>: ListServiceType where ListType: ListModelType, ListType.Item: Mappable {
typealias BaseListModel = ListType
typealias BaseItem = ListType.Item
struct ListResponse {
var listModel: BaseListModel?
var items: [BaseItem] = []
var pagination: PaginationModel?
var refreshStatus: RefreshStatus?
}
private var page = 1
private var isPaging = true
var targetTransform: PagingTargetTransform
var request = PublishSubject<PagingRequestType>()
var refreshResult: Observable<RefreshResult> {
let refreshResult = _listResponse.flatMap { listResponse -> Observable<RefreshResult> in
guard let refreshStatus = listResponse.refreshStatus else {
return Observable.empty()
}
return Observable.just((refreshStatus, listResponse.items.isEmpty))
}
return Observable.merge(refreshResult, _refreshResult)
}
var listResponse: Observable<ListResponse> {
return _listResponse
}
var response: Observable<[BaseItem]> {
let itemResponse = _listResponse.map { $0.items }
return Observable.merge(itemResponse, _localResponse)
}
var responseModel: Observable<BaseListModel> {
return _listResponse.flatMap { listResponse -> Observable<BaseListModel> in
guard let listModel = listResponse.listModel else {
return Observable.empty()
}
return Observable.just(listModel)
}
}
var pagination: Observable<PaginationModel?> {
return _listResponse.map { $0.pagination }
}
var error: Observable<Error> {
return _error.asObserver()
}
var executing: Observable<Bool> {
return _executing.asObserver()
}
var isExecuting = false
var listResponseModel: ListResponse?
var listModel: BaseListModel? {
return listResponseModel?.listModel
}
var paginationModel: PaginationModel? {
return listResponseModel?.pagination
}
var items: [BaseItem] {
return listResponseModel?.items ?? []
}
private var _listResponse: Observable<ListResponse> = .empty()
private let _localResponse = PublishSubject<[BaseItem]>()
private let _refreshResult = PublishSubject<RefreshResult>()
private let _error = PublishSubject<Error>()
private let _executing = PublishSubject<Bool>()
// MARK: API
var fileterMap: ((PagingRequestType) -> FilterMap<Int>) {
return { [unowned self] request -> FilterMap<Int> in
switch request {
case .refresh:
//
self.page = 1
self.isExecuting = true
self._executing.onNext(self.isExecuting)
return .map(self.page)
case .more:
//
if self.items.isEmpty {
self.page = 1
}
self.page += 1
self.isExecuting = true
self._executing.onNext(self.isExecuting)
return .map(self.page)
case .clear:
//
self.onNext([])
return .ignore
}
}
}
var transform: ((Event<BaseListModel>) -> ListResponse) {
return { [weak self] (event) in
var listResponse = ListResponse()
if let items = self?.listResponseModel?.items {
listResponse.items = items
}
guard let self = self else {
return listResponse
}
switch event {
case let .next(newListModel):
listResponse.listModel = newListModel
// New data arrived
let newItems = newListModel.list
// Paging enable
if self.isPaging {
if self.page == 1 {
if newItems.isEmpty {
listResponse.items = []
} else {
listResponse.items = newItems
}
} else {
listResponse.items.append(contentsOf: newItems)
}
// Paging info
listResponse.pagination = newListModel.pagination
if newListModel.pagination!.hasMoreData() {
listResponse.refreshStatus = .hasMoreData
} else {
listResponse.refreshStatus = .noMoreData
}
} else {
if newItems.isEmpty {
listResponse.items = []
} else {
listResponse.items = newItems
}
listResponse.refreshStatus = .noMoreData
}
default:
break
}
self.listResponseModel = listResponse
return listResponse
}
}
var filter: ((Event<BaseListModel>) -> Bool) {
return { [weak self] (event) in
guard let self = self else {
return false
}
switch event {
case let .error(error):
if self.page != 1 {
self.page -= 1
}
self.isExecuting = false
self._executing.onNext(self.isExecuting)
self._refreshResult.onNext((.invalidData, self.items.isEmpty))
self._error.onNext(error)
return false
case .completed:
return false
case .next:
self.isExecuting = false
self._executing.onNext(self.isExecuting)
return true
}
}
}
@available(*, deprecated, message: "将弃用请使用init(newPaging: Bool = true, targetTransform: @escaping PagingTargetTransform)")
init(paging: Bool = true, targetTransform: @escaping PagingTargetTransform) {
self.isPaging = paging
self.targetTransform = targetTransform
_listResponse = request.filterMap(self.fileterMap)
.map { [weak self] page -> MultiTarget in
guard let self = self else { return targetTransform(page) }
return self.targetTransform(page)
}.flatMapLatest { [weak self] api -> Observable<ListResponse> in
guard let self = self else {
return Observable.empty()
}
return self.request(api: api)
.materialize()
.filter(self.filter)
.map(self.transform)
}.share(replay: 1)
}
init(newPaging: Bool = true, targetTransform: @escaping PagingTargetTransform) {
self.isPaging = newPaging
self.targetTransform = targetTransform
_listResponse = request.filterMap(self.fileterMap)
.map { [weak self] page -> MultiTarget in
guard let self = self else { return targetTransform(page) }
return self.targetTransform(page)
}.flatMapLatest { [weak self] api -> Observable<ListResponse> in
guard let self = self else {
return Observable.empty()
}
return self.requestList(api: api)
.materialize()
.filter(self.filter)
.map(self.transform)
}.share(replay: 1)
}
func request(api: MultiTarget) -> Observable<BaseListModel> {
if self.isPaging == false {
return APIProvider.request(token: api)
.map(ListType.self)
.asObservable()
}
return APIProvider.request(token: api)
.map(ListType.self)//, atKeyPath: "result")
.asObservable()
}
func requestList(api: MultiTarget) -> Observable<BaseListModel> {
if self.isPaging == false {
return APIProvider.request(token: api).map(ListType.self).asObservable()
}
return APIProvider.request(token: api).map(ListType.self).asObservable()
}
func onNext(_ value: [BaseItem]) {
listResponseModel?.items = value
_localResponse.onNext(value)
}
}
// MARK: - Map array elements to animatable model
extension Array where Element: IdentifiableType & Equatable {
func mapAnimatableSection() -> [AnimatableSectionModel<String, Element>] {
guard !isEmpty else { return [] }
let newItems = removedDuplicates()
return [AnimatableSectionModel(model: "", items: newItems)]
}
}
extension Array where Element: IdentifiableType {
func removedDuplicates() -> [Element] {
// remove duplicated item
var uuids = Set<AnyHashable>()
var newItems: [Element] = []
for item in self {
let identity = item.identity
if !uuids.contains(identity) {
_ = uuids.insert(identity)
newItems.append(item)
}
}
return newItems
}
}
// MARK: - Map array elements to section model
extension Array {
func mapSection() -> [SectionModel<String, Element>] {
guard !isEmpty else { return [] }
return [SectionModel(model: "", items: self)]
}
}
// MARK: - Differ animatable section model
extension ListService where BaseItem: IdentifiableType & Equatable {
var animatableSectionedItems: Observable<[AnimatableSectionModel<String, BaseItem>]> {
return response.map { items -> [AnimatableSectionModel<String, BaseItem>] in
return items.mapAnimatableSection()
}
}
}
// MARK: - Section model
extension ListService {
var sectionedItems: Observable<[SectionModel<String, BaseItem>]> {
return response.map { items -> [SectionModel<String, BaseItem>] in
return items.mapSection()
}
}
}