170 lines
5.8 KiB
Swift
170 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 self.item.read != read else { return }
|
|
Task {
|
|
await self.delegate?.fervorController.markItem(self.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()
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|