Tusker/Tusker/Screens/Timeline/TimelineGapCollectionViewCe...

198 lines
5.8 KiB
Swift

//
// 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()
}
}