// Copyright © 2020 Metabolist. All rights reserved. import Combine import UIKit 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 super.init(frame: .zero) initialSetup() applyLoadMoreConfiguration() } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension LoadMoreView { static let accessibilityCustomAction = Notification.Name("com.metabolist.metatext.load-more-view.accessibility-custom-action") static var estimatedHeight: CGFloat { .defaultSpacing * 2 + UIFont.preferredFont(forTextStyle: .title2).lineHeight } 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 } set { guard let loadMoreConfiguration = newValue as? LoadMoreContentConfiguration else { return } self.loadMoreConfiguration = loadMoreConfiguration applyLoadMoreConfiguration() } } } private extension LoadMoreView { static let directionChangeMax = CGFloat.pi static let directionChangeIncrement = CGFloat.pi / 10 // swiftlint:disable:next function_body_length func initialSetup() { for arrowImageView in [leadingArrowImageView, trailingArrowImageView] { addSubview(arrowImageView) arrowImageView.translatesAutoresizingMaskIntoConstraints = false arrowImageView.image = UIImage( systemName: "arrow.up", withConfiguration: UIImage.SymbolConfiguration( pointSize: UIFont.preferredFont(forTextStyle: .title2).pointSize)) arrowImageView.contentMode = .scaleAspectFit arrowImageView.setContentHuggingPriority(.required, for: .horizontal) } addSubview(label) label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center label.font = .preferredFont(forTextStyle: .title2) label.adjustsFontForContentSizeCategory = true label.textColor = label.tintColor label.text = NSLocalizedString("load-more", comment: "") label.setContentHuggingPriority(.defaultLow, for: .horizontal) addSubview(activityIndicatorView) activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false activityIndicatorView.hidesWhenStopped = true NSLayoutConstraint.activate([ heightAnchor.constraint(greaterThanOrEqualToConstant: .minimumButtonDimension * 2), leadingArrowImageView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), leadingArrowImageView.topAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.topAnchor), leadingArrowImageView.bottomAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.bottomAnchor), leadingArrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor), label.leadingAnchor.constraint(equalTo: leadingArrowImageView.trailingAnchor), label.topAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.topAnchor), label.bottomAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.bottomAnchor), label.trailingAnchor.constraint(equalTo: trailingArrowImageView.leadingAnchor), label.centerYAnchor.constraint(equalTo: centerYAnchor), trailingArrowImageView.topAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.topAnchor), trailingArrowImageView.bottomAnchor.constraint(greaterThanOrEqualTo: readableContentGuide.bottomAnchor), trailingArrowImageView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), trailingArrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor), activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor), activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor) ]) isAccessibilityElement = true accessibilityLabel = NSLocalizedString("load-more", comment: "") let aboveAccessibilityActionName: String let belowAccessibilityActionName: String switch loadMoreConfiguration.viewModel.identityContext.appPreferences.statusWord { case .toot: aboveAccessibilityActionName = NSLocalizedString("load-more.above.accessibility.toot", comment: "") belowAccessibilityActionName = NSLocalizedString("load-more.below.accessibility.toot", comment: "") case .post: aboveAccessibilityActionName = NSLocalizedString("load-more.above.accessibility.post", comment: "") belowAccessibilityActionName = NSLocalizedString("load-more.below.accessibility.post", comment: "") } accessibilityCustomActions = [ UIAccessibilityCustomAction( name: aboveAccessibilityActionName) { [weak self] _ in self?.directionChange = -Self.directionChangeMax self?.updateDirectionChange(animated: false) NotificationCenter.default.post(name: Self.accessibilityCustomAction, object: self) return true }, UIAccessibilityCustomAction( name: belowAccessibilityActionName) { [weak self] _ in self?.directionChange = Self.directionChangeMax self?.updateDirectionChange(animated: false) NotificationCenter.default.post(name: Self.accessibilityCustomAction, object: self) return true } ] } func applyLoadMoreConfiguration() { 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) } }