// // TimelineGapCollectionViewCell.swift // Tusker // // Created by Shadowfacts on 11/16/22. // Copyright © 2022 Shadowfacts. All rights reserved. // import UIKit class TimelineGapCollectionViewCell: UICollectionViewCell { private(set) var direction = TimelineGapDirection.above private let indicator = UIActivityIndicatorView(style: .medium) private let chevronView = AnimatingChevronView() var fillGap: ((TimelineGapDirection) async -> Void)? override var isHighlighted: Bool { didSet { backgroundColor = isHighlighted ? .appFill : .appGroupedBackground } } var showsIndicator: Bool = false { didSet { if showsIndicator { indicator.isHidden = false indicator.startAnimating() } else { indicator.isHidden = true indicator.stopAnimating() } } } override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .appGroupedBackground indicator.isHidden = true indicator.color = .tintColor let label = UILabel() label.text = "Load more" label.font = .preferredFont(forTextStyle: .headline) label.adjustsFontForContentSizeCategory = true label.textColor = .tintColor chevronView.update(direction: .above) let stack = UIStackView(arrangedSubviews: [ label, chevronView, ]) stack.axis = .horizontal stack.spacing = 8 stack.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(stack) indicator.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(indicator) NSLayoutConstraint.activate([ stack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), stack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), indicator.trailingAnchor.constraint(equalTo: stack.leadingAnchor, constant: -8), indicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), contentView.heightAnchor.constraint(equalToConstant: 44), ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func didMoveToSuperview() { super.didMoveToSuperview() update() } func update() { guard let superview = superview as? UICollectionView else { return } let yInParent = frame.minY - superview.contentOffset.y let centerInParent = yInParent + bounds.height / 2 let centerOfParent = superview.frame.height / 2 let newDirection: TimelineGapDirection if centerInParent > centerOfParent { newDirection = .above } else { newDirection = .below } if newDirection != direction { direction = newDirection chevronView.update(direction: newDirection) } } // MARK: Accessibility override var isAccessibilityElement: Bool { get { true } set {} } override var accessibilityLabel: String? { get { "Load \(direction.accessibilityLabel)" } set {} } override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { get { [TimelineGapDirection.below, .above].map { dir in UIAccessibilityCustomAction(name: "Load \(dir.accessibilityLabel)") { [unowned self] _ in Task { showsIndicator = true await fillGap?(dir) showsIndicator = false } return true } } } set {} } } private class AnimatingChevronView: UIView { override class var layerClass: AnyClass { CAShapeLayer.self } var shapeLayer: CAShapeLayer { layer as! CAShapeLayer } override var intrinsicContentSize: CGSize { CGSize(width: 20, height: 25) } var animator: UIViewPropertyAnimator? let upPath: CGPath = { let path = UIBezierPath() let width: CGFloat = 20 let height: CGFloat = 25 path.move(to: CGPoint(x: 0, y: height / 2)) path.addLine(to: CGPoint(x: width / 2, y: height / 5)) path.addLine(to: CGPoint(x: width, y: height / 2)) return path.cgPath }() let downPath: CGPath = { let path = UIBezierPath() let width: CGFloat = 20 let height: CGFloat = 25 path.move(to: CGPoint(x: 0, y: height / 2)) path.addLine(to: CGPoint(x: width / 2, y: 4 * height / 5)) path.addLine(to: CGPoint(x: width, y: height / 2)) return path.cgPath }() init() { super.init(frame: .zero) shapeLayer.fillColor = nil shapeLayer.strokeColor = tintColor.cgColor shapeLayer.lineCap = .round shapeLayer.lineJoin = .round shapeLayer.lineWidth = 3 } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func tintColorDidChange() { super.tintColorDidChange() shapeLayer.strokeColor = tintColor.cgColor } func update(direction: TimelineGapDirection) { if animator?.isRunning == true { animator!.stopAnimation(true) } animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear) { if direction == .below { self.shapeLayer.path = self.upPath } else { self.shapeLayer.path = self.downPath } } animator!.startAnimation() } }