commit
ded9e878eb
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "LeadKit"
|
||||
s.version = "0.5.4"
|
||||
s.version = "0.5.5"
|
||||
s.summary = "iOS framework with a bunch of tools for rapid development"
|
||||
s.homepage = "https://github.com/TouchInstinct/LeadKit"
|
||||
s.license = "Apache License, Version 2.0"
|
||||
|
|
|
|||
|
|
@ -142,6 +142,11 @@ where Delegate.Cursor == Cursor {
|
|||
paginationViewModel.load(.reload)
|
||||
}
|
||||
|
||||
/// Method acts like reload, but shows initial loading view after being invoked.
|
||||
public func retry() {
|
||||
paginationViewModel.load(.retry)
|
||||
}
|
||||
|
||||
/// Method that enables placeholders animation due pull-to-refresh interaction.
|
||||
///
|
||||
/// - Parameter scrollObservable: Observable that emits content offset as CGPoint.
|
||||
|
|
@ -204,7 +209,9 @@ where Delegate.Cursor == Cursor {
|
|||
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
|
||||
addInfiniteScroll()
|
||||
if !cursor.exhausted {
|
||||
addInfiniteScroll()
|
||||
}
|
||||
} else if case .loadingMore = afterState {
|
||||
delegate?.paginationWrapper(wrapper: self, didLoad: newItems, usingCursor: cursor)
|
||||
|
||||
|
|
@ -214,15 +221,15 @@ where Delegate.Cursor == Cursor {
|
|||
|
||||
private func onErrorState(error: Error, afterState: PaginationViewModel<Cursor>.State) {
|
||||
if case .loading = afterState {
|
||||
enterPlaceholderState()
|
||||
defer {
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
}
|
||||
|
||||
guard let errorView = delegate?.errorPlaceholder(forPaginationWrapper: self, forError: error) else {
|
||||
return
|
||||
}
|
||||
|
||||
preparePlaceholderView(errorView)
|
||||
|
||||
currentPlaceholderView = errorView
|
||||
replacePlaceholderViewIfNeeded(with: errorView)
|
||||
} else if case .loadingMore = afterState {
|
||||
removeInfiniteScroll()
|
||||
|
||||
|
|
@ -244,15 +251,42 @@ where Delegate.Cursor == Cursor {
|
|||
}
|
||||
|
||||
private func onEmptyState() {
|
||||
enterPlaceholderState()
|
||||
|
||||
defer {
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
}
|
||||
guard let emptyView = delegate?.emptyPlaceholder(forPaginationWrapper: self) else {
|
||||
return
|
||||
}
|
||||
replacePlaceholderViewIfNeeded(with: emptyView)
|
||||
}
|
||||
|
||||
preparePlaceholderView(emptyView)
|
||||
private func replacePlaceholderViewIfNeeded(with placeholderView: UIView) {
|
||||
// don't update placeholder view if previous placeholder is the same one
|
||||
if currentPlaceholderView === placeholderView {
|
||||
return
|
||||
}
|
||||
tableView.isUserInteractionEnabled = true
|
||||
removeCurrentPlaceholderView()
|
||||
|
||||
currentPlaceholderView = emptyView
|
||||
placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderView.isHidden = false
|
||||
|
||||
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
|
||||
let wrapperView = UIView()
|
||||
wrapperView.addSubview(placeholderView)
|
||||
|
||||
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: wrapperView.leadingAnchor)
|
||||
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: wrapperView.trailingAnchor)
|
||||
let topConstraint = placeholderView.topAnchor.constraint(equalTo: wrapperView.topAnchor)
|
||||
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: wrapperView.bottomAnchor)
|
||||
|
||||
wrapperView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
|
||||
|
||||
currentPlaceholderViewTopConstraint = topConstraint
|
||||
|
||||
tableView.backgroundView = wrapperView
|
||||
|
||||
currentPlaceholderView = placeholderView
|
||||
}
|
||||
|
||||
// MARK: - private stuff
|
||||
|
|
@ -321,33 +355,6 @@ where Delegate.Cursor == Cursor {
|
|||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
private func enterPlaceholderState() {
|
||||
tableView.support.refreshControl?.endRefreshing()
|
||||
tableView.isUserInteractionEnabled = true
|
||||
|
||||
removeCurrentPlaceholderView()
|
||||
}
|
||||
|
||||
private func preparePlaceholderView(_ placeholderView: UIView) {
|
||||
placeholderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
placeholderView.isHidden = false
|
||||
|
||||
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
|
||||
let wrapperView = UIView()
|
||||
wrapperView.addSubview(placeholderView)
|
||||
|
||||
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: wrapperView.leadingAnchor)
|
||||
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: wrapperView.trailingAnchor)
|
||||
let topConstraint = placeholderView.topAnchor.constraint(equalTo: wrapperView.topAnchor)
|
||||
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: wrapperView.bottomAnchor)
|
||||
|
||||
wrapperView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
|
||||
|
||||
currentPlaceholderViewTopConstraint = topConstraint
|
||||
|
||||
tableView.backgroundView = wrapperView
|
||||
}
|
||||
|
||||
private func removeCurrentPlaceholderView() {
|
||||
tableView.backgroundView = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,9 +61,11 @@ public final class PaginationViewModel<C: ResettableCursorType> {
|
|||
///
|
||||
/// - reload: reload all items and reset cursor to initial state.
|
||||
/// - next: load next batch of items.
|
||||
/// - retry: reload to initial loading state
|
||||
public enum LoadType {
|
||||
|
||||
case reload
|
||||
case retry
|
||||
case next
|
||||
|
||||
}
|
||||
|
|
@ -94,10 +96,9 @@ public final class PaginationViewModel<C: ResettableCursorType> {
|
|||
public func load(_ loadType: LoadType) {
|
||||
switch loadType {
|
||||
case .reload:
|
||||
currentRequest?.dispose()
|
||||
cursor = cursor.reset()
|
||||
|
||||
internalState.value = .loading(after: internalState.value)
|
||||
reload()
|
||||
case .retry:
|
||||
reload(isRetry: true)
|
||||
case .next:
|
||||
if case .exhausted(_) = internalState.value {
|
||||
fatalError("You shouldn't call load(.next) after got .exhausted state!")
|
||||
|
|
@ -118,6 +119,11 @@ public final class PaginationViewModel<C: ResettableCursorType> {
|
|||
}
|
||||
|
||||
private func onGot(newItems: [C.Element], using cursor: C) {
|
||||
if newItems.isEmpty {
|
||||
internalState.value = .empty
|
||||
return
|
||||
}
|
||||
|
||||
internalState.value = .results(newItems: newItems, inCursor: cursor, after: internalState.value)
|
||||
|
||||
if cursor.exhausted {
|
||||
|
|
@ -138,4 +144,14 @@ public final class PaginationViewModel<C: ResettableCursorType> {
|
|||
}
|
||||
}
|
||||
|
||||
private func reload(isRetry: Bool = false) {
|
||||
currentRequest?.dispose()
|
||||
cursor = cursor.reset()
|
||||
|
||||
if isRetry {
|
||||
internalState.value = .initial
|
||||
}
|
||||
internalState.value = .loading(after: internalState.value)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,18 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
/// Protocol that describes placeholder view, containing loading indicator.
|
||||
public protocol LoadingIndicatorHolder: class {
|
||||
var loadingIndicator: Animatable { get }
|
||||
var indicatorOwner: UIView { get }
|
||||
}
|
||||
|
||||
public extension LoadingIndicatorHolder where Self: UIView {
|
||||
public var indicatorOwner: UIView {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol that describes badic loading indicator.
|
||||
public protocol LoadingIndicator {
|
||||
|
||||
|
|
|
|||
|
|
@ -25,20 +25,28 @@ import UIKit
|
|||
/// Type that performs some kind of type erasure for LoadingIndicator.
|
||||
public struct AnyLoadingIndicator: Animatable {
|
||||
|
||||
private let internalView: UIView
|
||||
private let backgroundView: UIView
|
||||
private let animatableView: Animatable
|
||||
|
||||
/// Initializer with indicator that should be wrapped.
|
||||
///
|
||||
/// - Parameter _: indicator for wrapping.
|
||||
public init<Indicator> (_ base: Indicator) where Indicator: LoadingIndicator {
|
||||
self.internalView = base.view
|
||||
self.backgroundView = base.view
|
||||
self.animatableView = base.view
|
||||
}
|
||||
|
||||
/// The indicator view.
|
||||
/// Initializer with placeholder view, that wraps indicator.
|
||||
///
|
||||
/// - Parameter loadingIndicatorHolder: placeholder view, containing indicator.
|
||||
public init(loadingIndicatorHolder: LoadingIndicatorHolder) {
|
||||
self.backgroundView = loadingIndicatorHolder.indicatorOwner
|
||||
self.animatableView = loadingIndicatorHolder.loadingIndicator
|
||||
}
|
||||
|
||||
/// The background view.
|
||||
var view: UIView {
|
||||
return internalView
|
||||
return backgroundView
|
||||
}
|
||||
|
||||
public func startAnimating() {
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ class CursorTests: XCTestCase {
|
|||
XCTAssertEqual(loadedItems.count, 40)
|
||||
|
||||
cursorExpectation.fulfill()
|
||||
}) { error in
|
||||
}, onError: { error in
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
})
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
waitForExpectations(timeout: 10, handler: nil)
|
||||
|
|
|
|||
Loading…
Reference in New Issue