// // ItemsViewController.swift // Reader // // Created by Shadowfacts on 1/9/22. // import UIKit import CoreData import SafariServices protocol ItemsViewControllerDelegate: AnyObject { func showReadItem(_ item: Item) } class ItemsViewController: UIViewController { weak var delegate: ItemsViewControllerDelegate? let fervorController: FervorController let fetchRequest: NSFetchRequest private var collectionView: UICollectionView! private var resultsController: NSFetchedResultsController! private var batchUpdates: [() -> Void] = [] init(fetchRequest: NSFetchRequest, fervorController: FervorController) { self.fervorController = fervorController self.fetchRequest = fetchRequest super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() if UIDevice.current.userInterfaceIdiom != .mac { view.backgroundColor = .appBackground } var configuration = UICollectionLayoutListConfiguration(appearance: .plain) configuration.backgroundColor = .clear let layout = UICollectionViewCompositionalLayout.list(using: configuration) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView.delegate = self collectionView.dataSource = self collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.register(ItemCollectionViewCell.self, forCellWithReuseIdentifier: "itemCell") view.addSubview(collectionView) NSLayoutConstraint.activate([ collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) fetchRequest.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)] fetchRequest.fetchBatchSize = 20 resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil) resultsController.delegate = self try! resultsController.performFetch() } } extension ItemsViewController: UICollectionViewDataSource { func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return resultsController.fetchedObjects?.count ?? 0 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "itemCell", for: indexPath) as! ItemCollectionViewCell cell.delegate = self cell.scrollView = collectionView cell.updateUI(item: resultsController.fetchedObjects![indexPath.row]) return cell } } extension ItemsViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { batchUpdates = [] } func controllerDidChangeContent(_ controller: NSFetchedResultsController) { collectionView.performBatchUpdates { for update in self.batchUpdates { update() } } // clear to prevent retain cycles batchUpdates = [] } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { guard let indexPath = indexPath else { return } batchUpdates.append { switch type { case .insert: self.collectionView.insertItems(at: [indexPath]) case .delete: self.collectionView.deleteItems(at: [indexPath]) case .move: if let newIndexPath = newIndexPath { self.collectionView.moveItem(at: indexPath, to: newIndexPath) } case .update: self.collectionView.reloadItems(at: [indexPath]) @unknown default: break } } } } extension ItemsViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let item = resultsController.fetchedObjects![indexPath.row] return UIContextMenuConfiguration(identifier: nil, previewProvider: { ReadViewController(item: item, fervorController: self.fervorController) }, actionProvider: { _ in var children: [UIAction] = [] if let url = item.url { children.append(UIAction(title: "Open in Safari", image: UIImage(systemName: "safari"), handler: { [weak self] _ in let vc = SFSafariViewController(url: url) vc.preferredControlTintColor = .appTintColor self?.present(vc, animated: true) })) children.append(UIAction(title: "Share", image: UIImage(systemName: "square.and.arrow.up"), handler: { [weak self] _ in self?.present(UIActivityViewController(activityItems: [url], applicationActivities: nil), animated: true) })) } if item.read { children.append(UIAction(title: "Mark as Unread", image: UIImage(systemName: "checkmark.circle"), handler: { [unowned self] _ in Task { await self.fervorController.markItem(item, read: false) } })) } else { children.append(UIAction(title: "Mark as Read", image: UIImage(systemName: "checkmark.circle.fill"), handler: { [unowned self] _ in Task { await self.fervorController.markItem(item, read: true) } })) } return UIMenu(children: children) }) } func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { guard let vc = animator.previewViewController else { return } animator.preferredCommitStyle = .pop animator.addCompletion { self.show(vc, sender: nil) } } } extension ItemsViewController: ItemCollectionViewCellDelegate { func itemCellSelected(cell: ItemCollectionViewCell, item: Item) { cell.setRead(true, animated: true) if let delegate = delegate { delegate.showReadItem(item) } else { show(ReadViewController(item: item, fervorController: fervorController), sender: nil) } } }