LeadKit/TIPagination/Sources/Default/DefaultPaginatorUIDelegate....

220 lines
7.2 KiB
Swift

//
// Copyright (c) 2020 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit
import TISwiftUtils
open class DefaultPaginatorUIDelegate<Cursor: PaginatorCursorType>: PaginatorUIDelegate {
public typealias ViewSetter = ParameterClosure<UIView?>
// MARK: - Private Properties
private let scrollView: UIScrollView
private let backgroundViewSetter: ViewSetter
private let footerViewSetter: ViewSetter
private var currentPlaceholderView: UIView?
// MARK: - Public Properties
/// Called when default retry button is pressed
public var onRetry: VoidClosure?
// MARK: - Public Initializers
public convenience init(_ tableView: UITableView) {
self.init(scrollView: tableView,
backgroundViewSetter: { [weak tableView] in
tableView?.backgroundView = $0
}, footerViewSetter: { [weak tableView] in
tableView?.tableFooterView = $0
})
}
public convenience init(_ collectionView: UICollectionView) {
self.init(scrollView: collectionView,
backgroundViewSetter: { [weak collectionView] in
collectionView?.backgroundView = $0
}, footerViewSetter: { _ in
// No footer in UICollectionView
})
}
public init(scrollView: UIScrollView,
backgroundViewSetter: @escaping ViewSetter,
footerViewSetter: @escaping ViewSetter) {
self.scrollView = scrollView
self.backgroundViewSetter = backgroundViewSetter
self.footerViewSetter = footerViewSetter
}
// MARK: - UI Setup
open func footerRetryView() -> UIView? {
let retryButton = UIButton(type: .custom)
retryButton.backgroundColor = .lightGray
retryButton.setTitle("Retry load more", for: .normal)
retryButton.addTarget(self, action: #selector(retryAction), for: .touchUpInside)
return retryButton
}
open func footerRetryViewHeight() -> CGFloat {
44
}
open func emptyPlaceholder() -> UIView? {
let label = UILabel()
label.text = "Empty"
return label
}
open func errorPlaceholder(for error: Cursor.Failure) -> UIView? {
let label = UILabel()
label.text = error.localizedDescription
return label
}
// MARK: - PaginatorUIDelegate
open func onInitialLoading() {
scrollView.isUserInteractionEnabled = false
removeAllPlaceholders()
let loadingIndicatorView = UIActivityIndicatorView()
loadingIndicatorView.translatesAutoresizingMaskIntoConstraints = true
backgroundViewSetter(loadingIndicatorView)
loadingIndicatorView.startAnimating()
currentPlaceholderView = loadingIndicatorView
}
open func onReloading() {
footerViewSetter(nil)
}
open func onLoadingMore() {
footerViewSetter(nil)
}
open func onLoadingError(_ error: Cursor.Failure) {
guard let errorView = errorPlaceholder(for: error) else {
return
}
replacePlaceholderViewIfNeeded(with: errorView)
scrollView.refreshControl?.endRefreshing()
}
open func onLoadingMoreError(_ error: Cursor.Failure) {
guard let retryView = footerRetryView() else {
return
}
let retryViewHeight = footerRetryViewHeight()
retryView.frame = CGRect(x: 0,
y: 0,
width: scrollView.bounds.width,
height: retryViewHeight)
footerViewSetter(retryView)
let contentOffsetWithRetryView = scrollView.contentOffset.y + retryViewHeight
let invisibleContentHeight = scrollView.contentSize.height - scrollView.frame.size.height
let shouldUpdateContentOffset = contentOffsetWithRetryView >= invisibleContentHeight
if shouldUpdateContentOffset {
scrollView.setContentOffset(CGPoint(x: 0, y: contentOffsetWithRetryView),
animated: true)
}
}
open func onSuccessfulLoad() {
scrollView.isUserInteractionEnabled = true
removeAllPlaceholders()
scrollView.refreshControl?.endRefreshing()
}
open func onEmptyState() {
guard let emptyView = emptyPlaceholder() else {
return
}
replacePlaceholderViewIfNeeded(with: emptyView)
scrollView.refreshControl?.endRefreshing()
}
open func onExhaustedState() {
removeAllPlaceholders()
}
open func onAddInfiniteScroll() {
// empty
}
// MARK: - Private Methods
private func replacePlaceholderViewIfNeeded(with placeholderView: UIView) {
scrollView.isUserInteractionEnabled = true
removeAllPlaceholders()
placeholderView.translatesAutoresizingMaskIntoConstraints = false
placeholderView.isHidden = false
// I was unable to add pull-to-refresh placeholder scroll behaviour without this trick
let placeholderWrapperView = UIView()
placeholderWrapperView.addSubview(placeholderView)
let leadingConstraint = placeholderView.leadingAnchor.constraint(equalTo: placeholderWrapperView.leadingAnchor)
let trailingConstraint = placeholderView.trailingAnchor.constraint(equalTo: placeholderWrapperView.trailingAnchor)
let topConstraint = placeholderView.topAnchor.constraint(equalTo: placeholderWrapperView.topAnchor)
let bottomConstraint = placeholderView.bottomAnchor.constraint(equalTo: placeholderWrapperView.bottomAnchor)
NSLayoutConstraint.activate([
leadingConstraint,
trailingConstraint,
topConstraint,
bottomConstraint
])
backgroundViewSetter(placeholderWrapperView)
currentPlaceholderView = placeholderView
}
private func removeAllPlaceholders() {
backgroundViewSetter(nil)
footerViewSetter(nil)
}
@objc private func retryAction() {
onRetry?()
}
}