diff --git a/LeadKit.podspec b/LeadKit.podspec index 63b59d06..389b855d 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -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" diff --git a/Sources/Classes/Pagination/PaginationTableViewWrapper.swift b/Sources/Classes/Pagination/PaginationTableViewWrapper.swift index a879914a..e0b2e591 100644 --- a/Sources/Classes/Pagination/PaginationTableViewWrapper.swift +++ b/Sources/Classes/Pagination/PaginationTableViewWrapper.swift @@ -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 is SingleLoadCursor) { + 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.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,23 @@ 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) - - currentPlaceholderView = emptyView + private func replacePlaceholderViewIfNeeded(with view: UIView) { + // don't update placeholder view if previous placeholder is the same one + if currentPlaceholderView === view { + return + } + enterPlaceholderState() + preparePlaceholderView(view) + currentPlaceholderView = view } // MARK: - private stuff @@ -322,7 +337,6 @@ where Delegate.Cursor == Cursor { } private func enterPlaceholderState() { - tableView.support.refreshControl?.endRefreshing() tableView.isUserInteractionEnabled = true removeCurrentPlaceholderView() diff --git a/Sources/Classes/Pagination/PaginationViewModel.swift b/Sources/Classes/Pagination/PaginationViewModel.swift index 59d3fa03..d5de6da9 100644 --- a/Sources/Classes/Pagination/PaginationViewModel.swift +++ b/Sources/Classes/Pagination/PaginationViewModel.swift @@ -63,7 +63,11 @@ public final class PaginationViewModel { /// - next: load next batch of items. public enum LoadType { + /// pull-to-refresh case reload + /// retry button inside placeholder + case retry + case next } @@ -94,10 +98,9 @@ public final class PaginationViewModel { 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 +121,11 @@ public final class PaginationViewModel { } 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 +146,14 @@ public final class PaginationViewModel { } } + private func reload(isRetry: Bool = false) { + currentRequest?.dispose() + cursor = cursor.reset() + + if isRetry { + internalState.value = .initial + } + internalState.value = .loading(after: internalState.value) + } + } diff --git a/Sources/Structures/Views/AnyLoadingIndicator.swift b/Sources/Structures/Views/AnyLoadingIndicator.swift index 3e5fbbcb..ebdf7422 100644 --- a/Sources/Structures/Views/AnyLoadingIndicator.swift +++ b/Sources/Structures/Views/AnyLoadingIndicator.swift @@ -31,8 +31,8 @@ public struct AnyLoadingIndicator: Animatable { /// Initializer with indicator that should be wrapped. /// /// - Parameter _: indicator for wrapping. - public init (_ base: Indicator) where Indicator: LoadingIndicator { - self.internalView = base.view + public init (_ base: Indicator, backgroundView: UIView? = nil) where Indicator: LoadingIndicator { + self.internalView = backgroundView ?? base.view self.animatableView = base.view } diff --git a/Tests/CursorTests.swift b/Tests/CursorTests.swift index fdb6e08c..cba3e0bd 100644 --- a/Tests/CursorTests.swift +++ b/Tests/CursorTests.swift @@ -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)