Merge pull request #84 from TouchInstinct/fix/wrapper

Fix/wrapper
This commit is contained in:
Ivan Zinovyev 2017-09-08 18:48:13 +03:00 committed by GitHub
commit ded9e878eb
6 changed files with 90 additions and 47 deletions

View File

@ -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"

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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() {

View File

@ -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)