frenzy-ios/Reader/Screens/Items/ItemCollectionViewCell.swift

173 lines
5.8 KiB
Swift

//
// ItemCollectionViewCell.swift
// Reader
//
// Created by Shadowfacts on 1/9/22.
//
import UIKit
protocol ItemCollectionViewCellDelegate: AnyObject {
var fervorController: FervorController { get }
func itemCellSelected(cell: ItemCollectionViewCell, item: Item)
}
class ItemCollectionViewCell: UICollectionViewListCell {
weak var delegate: ItemCollectionViewCellDelegate?
private let titleLabel = UILabel()
private let feedTitleLabel = UILabel()
private let contentLabel = UILabel()
private var shouldInteractOnNextTouch = true
var scrollView: UIScrollView?
private var item: Item!
private lazy var feedbackGenerator = UISelectionFeedbackGenerator()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundConfiguration = .clear()
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).withSymbolicTraits(.traitBold)!.withDesign(.serif)!
titleLabel.font = UIFont(descriptor: descriptor, size: 0)
titleLabel.numberOfLines = 0
feedTitleLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
contentLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).withDesign(.serif)!, size: 0)
contentLabel.numberOfLines = 0
let stack = UIStackView(arrangedSubviews: [
titleLabel,
feedTitleLabel,
contentLabel,
])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.spacing = 8
stack.axis = .vertical
addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
stack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
separatorLayoutGuide.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
])
let doubleTap = DoubleTapRecognizer(target: self, action: #selector(cellDoubleTapped))
doubleTap.onTouchesBegan = onDoubleTapBegan
addGestureRecognizer(doubleTap)
let singleTap = UITapGestureRecognizer(target: self, action: #selector(cellSingleTapped))
singleTap.require(toFail: doubleTap)
addGestureRecognizer(singleTap)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateUI(item: Item) {
self.item = item
titleLabel.text = item.title
feedTitleLabel.text = item.feed!.title ?? item.feed!.url?.host
if let excerpt = item.excerpt {
contentLabel.text = excerpt
contentLabel.isHidden = false
} else {
contentLabel.isHidden = true
}
updateColors()
}
func setRead(_ read: Bool, animated: Bool) {
guard item.read != read else { return }
item.read = read
if animated {
// i don't know why .transition works but .animate doesn't
UIView.transition(with: self, duration: 0.2, options: .transitionCrossDissolve) {
self.updateColors()
}
} else {
updateColors()
}
Task {
await delegate?.fervorController.markItem(item, read: read)
}
}
private func updateColors() {
if item.read {
titleLabel.textColor = .secondaryLabel
feedTitleLabel.textColor = .secondaryLabel
contentLabel.textColor = .secondaryLabel
} else {
titleLabel.textColor = .label
feedTitleLabel.textColor = .tintColor
contentLabel.textColor = .appContentPreviewLabel
}
}
private func onDoubleTapBegan() {
guard shouldInteractOnNextTouch else { return }
shouldInteractOnNextTouch = false
feedbackGenerator.prepare()
// couldn't manage to do this with just the recognizers
if scrollView?.isDragging == false && scrollView?.isDecelerating == false {
UIView.animateKeyframes(withDuration: 0.4, delay: 0, options: .allowUserInteraction) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
self.backgroundColor = .appCellHighlightBackground
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.backgroundColor = nil
}
}
}
}
@objc private func cellDoubleTapped() {
setRead(!item.read, animated: true)
shouldInteractOnNextTouch = true
feedbackGenerator.selectionChanged()
}
@objc private func cellSingleTapped() {
delegate?.itemCellSelected(cell: self, item: item)
shouldInteractOnNextTouch = true
feedbackGenerator.selectionChanged()
}
}
private class DoubleTapRecognizer: UITapGestureRecognizer {
var onTouchesBegan: (() -> Void)!
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
numberOfTapsRequired = 2
delaysTouchesBegan = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
onTouchesBegan()
// shorten the delay before the single tap is recognized
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in
guard let self = self else { return }
if self.state != .recognized {
self.state = .failed
}
}
}
}