// // ItemCollectionViewCell.swift // Reader // // Created by Shadowfacts on 1/9/22. // import UIKit protocol ItemCollectionViewCellDelegate: AnyObject { 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 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 } 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() 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, with event: UIEvent) { super.touchesBegan(touches, with: event) onTouchesBegan() // shorten the delay before the single tap is recognized DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { [weak self] in guard let self = self else { return } if self.state != .recognized { self.state = .failed } } } }