diff --git a/View Controllers/TableViewController.swift b/View Controllers/TableViewController.swift index be32381..c225aca 100644 --- a/View Controllers/TableViewController.swift +++ b/View Controllers/TableViewController.swift @@ -77,6 +77,22 @@ class TableViewController: UITableViewController { 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, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { @@ -101,6 +117,8 @@ class TableViewController: UITableViewController { } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } viewModel.itemSelected(item) @@ -160,6 +178,10 @@ extension TableViewController { } private extension TableViewController { + var visibleLoadMoreViews: [LoadMoreView] { + tableView.visibleCells.compactMap { $0.contentView as? LoadMoreView } + } + func setupViewModelBindings() { viewModel.title.sink { [weak self] in self?.navigationItem.title = $0 }.store(in: &cancellables) diff --git a/ViewModels/Sources/ViewModels/LoadMoreViewModel.swift b/ViewModels/Sources/ViewModels/LoadMoreViewModel.swift index bc91fec..6fac3ad 100644 --- a/ViewModels/Sources/ViewModels/LoadMoreViewModel.swift +++ b/ViewModels/Sources/ViewModels/LoadMoreViewModel.swift @@ -3,17 +3,16 @@ import Combine import ServiceLayer -public struct LoadMoreViewModel { - public let loading: AnyPublisher +final public class LoadMoreViewModel: ObservableObject { + public var direction = LoadMore.Direction.up + @Published public private(set) var loading = false public let events: AnyPublisher, Never> private let loadMoreService: LoadMoreService private let eventsSubject = PassthroughSubject, Never>() - private let loadingSubject = PassthroughSubject() init(loadMoreService: LoadMoreService) { self.loadMoreService = loadMoreService - loading = loadingSubject.eraseToAnyPublisher() events = eventsSubject.eraseToAnyPublisher() } } @@ -21,10 +20,10 @@ public struct LoadMoreViewModel { extension LoadMoreViewModel { func loadMore() { eventsSubject.send( - loadMoreService.request(direction: .down) + loadMoreService.request(direction: direction) .handleEvents( - receiveSubscription: { _ in loadingSubject.send(true) }, - receiveCompletion: { _ in loadingSubject.send(false) }) + receiveSubscription: { [weak self] _ in self?.loading = true }, + receiveCompletion: { [weak self] _ in self?.loading = false }) .map { _ in CollectionItemEvent.ignorableOutput } .eraseToAnyPublisher()) } diff --git a/Views/LoadMoreView.swift b/Views/LoadMoreView.swift index fef20ea..2836656 100644 --- a/Views/LoadMoreView.swift +++ b/Views/LoadMoreView.swift @@ -3,11 +3,14 @@ import Combine import UIKit -class LoadMoreView: UIView { +final class LoadMoreView: UIView { + private let leadingArrowImageView = UIImageView() + private let trailingArrowImageView = UIImageView() private let label = UILabel() private let activityIndicatorView = UIActivityIndicatorView() private var loadMoreConfiguration: LoadMoreContentConfiguration private var loadingCancellable: AnyCancellable? + private var directionChange = LoadMoreView.directionChangeMax init(configuration: LoadMoreContentConfiguration) { self.loadMoreConfiguration = configuration @@ -15,6 +18,7 @@ class LoadMoreView: UIView { super.init(frame: .zero) initialSetup() + applyLoadMoreConfiguration() } @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 { var configuration: UIContentConfiguration { get { loadMoreConfiguration } @@ -37,15 +61,15 @@ extension LoadMoreView: UIContentView { } private extension LoadMoreView { - func initialSetup() { - let leadingArrowImageView = UIImageView() - let trailingArrowImageView = UIImageView() + static let directionChangeMax = CGFloat.pi + static let directionChangeIncrement = CGFloat.pi / 10 + func initialSetup() { for arrowImageView in [leadingArrowImageView, trailingArrowImageView] { addSubview(arrowImageView) arrowImageView.translatesAutoresizingMaskIntoConstraints = false arrowImageView.image = UIImage( - systemName: "arrow.up.circle", + systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration( pointSize: UIFont.preferredFont(forTextStyle: .title2).pointSize)) arrowImageView.setContentHuggingPriority(.required, for: .horizontal) @@ -81,11 +105,27 @@ private extension LoadMoreView { } func applyLoadMoreConfiguration() { - loadingCancellable = loadMoreConfiguration.viewModel.loading.sink { [weak self] in + loadingCancellable = loadMoreConfiguration.viewModel.$loading.sink { [weak self] in guard let self = self else { return } self.label.isHidden = $0 $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) + } }