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

174 lines
5.8 KiB
Swift
Raw Normal View History

2022-01-09 23:34:17 +00:00
//
// ItemCollectionViewCell.swift
// Reader
//
// Created by Shadowfacts on 1/9/22.
//
import UIKit
import Persistence
2022-01-09 23:34:17 +00:00
2022-01-11 03:36:54 +00:00
protocol ItemCollectionViewCellDelegate: AnyObject {
var fervorController: FervorController { get }
2022-01-11 16:42:41 +00:00
func itemCellSelected(cell: ItemCollectionViewCell, item: Item)
2022-01-11 03:36:54 +00:00
}
2022-01-10 00:23:22 +00:00
class ItemCollectionViewCell: UICollectionViewListCell {
2022-01-09 23:34:17 +00:00
2022-01-11 03:36:54 +00:00
weak var delegate: ItemCollectionViewCellDelegate?
2022-01-09 23:34:17 +00:00
private let titleLabel = UILabel()
private let feedTitleLabel = UILabel()
private let contentLabel = UILabel()
2022-01-11 19:28:04 +00:00
private var shouldInteractOnNextTouch = true
var scrollView: UIScrollView?
2022-01-11 03:36:54 +00:00
private var item: Item!
2022-01-09 23:34:17 +00:00
2022-01-11 19:28:04 +00:00
private lazy var feedbackGenerator = UISelectionFeedbackGenerator()
2022-01-09 23:34:17 +00:00
override init(frame: CGRect) {
super.init(frame: frame)
2022-01-10 00:23:22 +00:00
backgroundConfiguration = .clear()
2022-01-09 23:34:17 +00:00
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),
2022-01-10 00:23:22 +00:00
separatorLayoutGuide.leadingAnchor.constraint(equalTo: stack.leadingAnchor),
2022-01-09 23:34:17 +00:00
])
2022-01-11 03:36:54 +00:00
let doubleTap = DoubleTapRecognizer(target: self, action: #selector(cellDoubleTapped))
2022-01-11 19:28:04 +00:00
doubleTap.onTouchesBegan = onDoubleTapBegan
2022-01-11 03:36:54 +00:00
addGestureRecognizer(doubleTap)
let singleTap = UITapGestureRecognizer(target: self, action: #selector(cellSingleTapped))
singleTap.require(toFail: doubleTap)
addGestureRecognizer(singleTap)
2022-01-09 23:34:17 +00:00
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateUI(item: Item) {
2022-01-11 03:36:54 +00:00
self.item = item
2022-01-09 23:34:17 +00:00
titleLabel.text = item.title
feedTitleLabel.text = item.feed!.title ?? item.feed!.url?.host
if let excerpt = item.excerpt {
contentLabel.text = excerpt
contentLabel.isHidden = false
2022-01-09 23:34:17 +00:00
} else {
contentLabel.isHidden = true
2022-01-09 23:34:17 +00:00
}
2022-01-11 03:36:54 +00:00
2022-01-11 16:42:41 +00:00
updateColors()
2022-01-11 03:36:54 +00:00
}
2022-01-11 16:42:41 +00:00
func setRead(_ read: Bool, animated: Bool) {
2022-01-15 19:49:29 +00:00
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()
2022-01-11 16:42:41 +00:00
}
2022-01-15 19:49:29 +00:00
} else {
updateColors()
}
Task {
await delegate?.fervorController.markItem(item, read: read)
2022-01-11 16:42:41 +00:00
}
}
private func updateColors() {
2022-01-11 03:36:54 +00:00
if item.read {
titleLabel.textColor = .secondaryLabel
feedTitleLabel.textColor = .secondaryLabel
contentLabel.textColor = .secondaryLabel
} else {
titleLabel.textColor = .label
feedTitleLabel.textColor = .tintColor
contentLabel.textColor = .appContentPreviewLabel
}
}
2022-01-11 19:28:04 +00:00
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
}
2022-01-11 03:36:54 +00:00
}
}
}
@objc private func cellDoubleTapped() {
2022-01-11 16:42:41 +00:00
setRead(!item.read, animated: true)
2022-01-11 19:28:04 +00:00
shouldInteractOnNextTouch = true
feedbackGenerator.selectionChanged()
2022-01-11 03:36:54 +00:00
}
@objc private func cellSingleTapped() {
2022-01-11 16:42:41 +00:00
delegate?.itemCellSelected(cell: self, item: item)
2022-01-11 19:28:04 +00:00
shouldInteractOnNextTouch = true
feedbackGenerator.selectionChanged()
2022-01-11 03:36:54 +00:00
}
}
2022-01-09 23:34:17 +00:00
2022-01-11 03:36:54 +00:00
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()
2022-01-11 19:28:04 +00:00
// shorten the delay before the single tap is recognized
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400)) { [weak self] in
2022-01-11 19:28:04 +00:00
guard let self = self else { return }
if self.state != .recognized {
self.state = .failed
}
}
2022-01-11 03:36:54 +00:00
}
2022-01-09 23:34:17 +00:00
}