Load more direction changing
This commit is contained in:
parent
7f937601b1
commit
90d750464b
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue