Load more direction changing

This commit is contained in:
Justin Mazzocchi 2020-10-04 18:25:02 -07:00
parent 7f937601b1
commit 90d750464b
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
3 changed files with 74 additions and 13 deletions

View file

@ -77,6 +77,22 @@ class TableViewController: UITableViewController {
viewModel.request(maxID: nil, minID: nil) viewModel.request(maxID: nil, minID: nil)
} }
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.isDragging else { return }
let up = scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0
for loadMoreView in visibleLoadMoreViews {
loadMoreView.directionChanged(up: up)
}
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
for loadMoreView in visibleLoadMoreViews {
loadMoreView.finalizeDirectionChange()
}
}
override func tableView(_ tableView: UITableView, override func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell, willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath) { forRowAt indexPath: IndexPath) {
@ -101,6 +117,8 @@ class TableViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let item = dataSource.itemIdentifier(for: indexPath) else { return } guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
viewModel.itemSelected(item) viewModel.itemSelected(item)
@ -160,6 +178,10 @@ extension TableViewController {
} }
private extension TableViewController { private extension TableViewController {
var visibleLoadMoreViews: [LoadMoreView] {
tableView.visibleCells.compactMap { $0.contentView as? LoadMoreView }
}
func setupViewModelBindings() { func setupViewModelBindings() {
viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables) viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables)

View file

@ -3,17 +3,16 @@
import Combine import Combine
import ServiceLayer import ServiceLayer
public struct LoadMoreViewModel { final public class LoadMoreViewModel: ObservableObject {
public let loading: AnyPublisher<Bool, Never> public var direction = LoadMore.Direction.up
@Published public private(set) var loading = false
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never> public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
private let loadMoreService: LoadMoreService private let loadMoreService: LoadMoreService
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>() private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
private let loadingSubject = PassthroughSubject<Bool, Never>()
init(loadMoreService: LoadMoreService) { init(loadMoreService: LoadMoreService) {
self.loadMoreService = loadMoreService self.loadMoreService = loadMoreService
loading = loadingSubject.eraseToAnyPublisher()
events = eventsSubject.eraseToAnyPublisher() events = eventsSubject.eraseToAnyPublisher()
} }
} }
@ -21,10 +20,10 @@ public struct LoadMoreViewModel {
extension LoadMoreViewModel { extension LoadMoreViewModel {
func loadMore() { func loadMore() {
eventsSubject.send( eventsSubject.send(
loadMoreService.request(direction: .down) loadMoreService.request(direction: direction)
.handleEvents( .handleEvents(
receiveSubscription: { _ in loadingSubject.send(true) }, receiveSubscription: { [weak self] _ in self?.loading = true },
receiveCompletion: { _ in loadingSubject.send(false) }) receiveCompletion: { [weak self] _ in self?.loading = false })
.map { _ in CollectionItemEvent.ignorableOutput } .map { _ in CollectionItemEvent.ignorableOutput }
.eraseToAnyPublisher()) .eraseToAnyPublisher())
} }

View file

@ -3,11 +3,14 @@
import Combine import Combine
import UIKit import UIKit
class LoadMoreView: UIView { final class LoadMoreView: UIView {
private let leadingArrowImageView = UIImageView()
private let trailingArrowImageView = UIImageView()
private let label = UILabel() private let label = UILabel()
private let activityIndicatorView = UIActivityIndicatorView() private let activityIndicatorView = UIActivityIndicatorView()
private var loadMoreConfiguration: LoadMoreContentConfiguration private var loadMoreConfiguration: LoadMoreContentConfiguration
private var loadingCancellable: AnyCancellable? private var loadingCancellable: AnyCancellable?
private var directionChange = LoadMoreView.directionChangeMax
init(configuration: LoadMoreContentConfiguration) { init(configuration: LoadMoreContentConfiguration) {
self.loadMoreConfiguration = configuration self.loadMoreConfiguration = configuration
@ -15,6 +18,7 @@ class LoadMoreView: UIView {
super.init(frame: .zero) super.init(frame: .zero)
initialSetup() initialSetup()
applyLoadMoreConfiguration()
} }
@available(*, unavailable) @available(*, unavailable)
@ -23,6 +27,26 @@ class LoadMoreView: UIView {
} }
} }
extension LoadMoreView {
func directionChanged(up: Bool) {
guard !loadMoreConfiguration.viewModel.loading else { return }
if up, directionChange < Self.directionChangeMax {
directionChange += Self.directionChangeIncrement
} else if !up, directionChange > -Self.directionChangeMax {
directionChange -= Self.directionChangeIncrement
}
updateDirectionChange(animated: false)
}
func finalizeDirectionChange() {
directionChange = directionChange > 0 ? Self.directionChangeMax : -Self.directionChangeMax
updateDirectionChange(animated: true)
}
}
extension LoadMoreView: UIContentView { extension LoadMoreView: UIContentView {
var configuration: UIContentConfiguration { var configuration: UIContentConfiguration {
get { loadMoreConfiguration } get { loadMoreConfiguration }
@ -37,15 +61,15 @@ extension LoadMoreView: UIContentView {
} }
private extension LoadMoreView { private extension LoadMoreView {
func initialSetup() { static let directionChangeMax = CGFloat.pi
let leadingArrowImageView = UIImageView() static let directionChangeIncrement = CGFloat.pi / 10
let trailingArrowImageView = UIImageView()
func initialSetup() {
for arrowImageView in [leadingArrowImageView, trailingArrowImageView] { for arrowImageView in [leadingArrowImageView, trailingArrowImageView] {
addSubview(arrowImageView) addSubview(arrowImageView)
arrowImageView.translatesAutoresizingMaskIntoConstraints = false arrowImageView.translatesAutoresizingMaskIntoConstraints = false
arrowImageView.image = UIImage( arrowImageView.image = UIImage(
systemName: "arrow.up.circle", systemName: "arrow.up",
withConfiguration: UIImage.SymbolConfiguration( withConfiguration: UIImage.SymbolConfiguration(
pointSize: UIFont.preferredFont(forTextStyle: .title2).pointSize)) pointSize: UIFont.preferredFont(forTextStyle: .title2).pointSize))
arrowImageView.setContentHuggingPriority(.required, for: .horizontal) arrowImageView.setContentHuggingPriority(.required, for: .horizontal)
@ -81,11 +105,27 @@ private extension LoadMoreView {
} }
func applyLoadMoreConfiguration() { func applyLoadMoreConfiguration() {
loadingCancellable = loadMoreConfiguration.viewModel.loading.sink { [weak self] in loadingCancellable = loadMoreConfiguration.viewModel.$loading.sink { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.label.isHidden = $0 self.label.isHidden = $0
$0 ? self.activityIndicatorView.startAnimating() : self.activityIndicatorView.stopAnimating() $0 ? self.activityIndicatorView.startAnimating() : self.activityIndicatorView.stopAnimating()
} }
} }
func updateDirectionChange(animated: Bool) {
if animated {
UIView.animate(withDuration: 0.1) {
self.performDirectionChangeUpdates()
}
} else {
self.performDirectionChangeUpdates()
}
}
func performDirectionChangeUpdates() {
loadMoreConfiguration.viewModel.direction = directionChange > 0 ? .up : .down
leadingArrowImageView.transform = CGAffineTransform(rotationAngle: .pi / 2 - directionChange / 2)
trailingArrowImageView.transform = CGAffineTransform(rotationAngle: -.pi / 2 + directionChange / 2)
}
} }