221 lines
9.4 KiB
Swift
221 lines
9.4 KiB
Swift
//
|
|
// ItemsViewController.swift
|
|
// Reader
|
|
//
|
|
// Created by Shadowfacts on 1/9/22.
|
|
//
|
|
|
|
import UIKit
|
|
import CoreData
|
|
import SafariServices
|
|
import Persistence
|
|
|
|
protocol ItemsViewControllerDelegate: AnyObject {
|
|
func showReadItem(_ item: Item)
|
|
}
|
|
|
|
class ItemsViewController: UIViewController {
|
|
|
|
weak var delegate: ItemsViewControllerDelegate?
|
|
|
|
let fervorController: FervorController
|
|
let type: ItemListType
|
|
private let fetchRequest: NSFetchRequest<NSManagedObjectID>
|
|
|
|
private var collectionView: UICollectionView!
|
|
private var dataSource: UICollectionViewDiffableDataSource<Section, NSManagedObjectID>!
|
|
|
|
private var batchUpdates: [() -> Void] = []
|
|
|
|
init(type: ItemListType, fervorController: FervorController) {
|
|
self.fervorController = fervorController
|
|
self.type = type
|
|
self.fetchRequest = type.idFetchRequest
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
self.title = type.title
|
|
switch type {
|
|
case .all:
|
|
self.userActivity = .readAll(account: fervorController.account!)
|
|
case .unread:
|
|
self.userActivity = .readUnread(account: fervorController.account!)
|
|
case .group(let group):
|
|
self.userActivity = .readGroup(group, account: fervorController.account!)
|
|
case .feed(let feed):
|
|
self.userActivity = .readFeed(feed, account: fervorController.account!)
|
|
}
|
|
}
|
|
|
|
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.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(collectionView)
|
|
|
|
dataSource = createDataSource()
|
|
|
|
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)]
|
|
fetchItems()
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: fervorController.persistentContainer.viewContext)
|
|
}
|
|
|
|
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, NSManagedObjectID> {
|
|
let itemCell = UICollectionView.CellRegistration<ItemCollectionViewCell, NSManagedObjectID> { [unowned self] cell, indexPath, itemIdentifier in
|
|
cell.delegate = self
|
|
cell.scrollView = self.collectionView
|
|
let item = self.fervorController.persistentContainer.viewContext.object(with: itemIdentifier) as! Item
|
|
cell.updateUI(item: item)
|
|
}
|
|
return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
|
|
return collectionView.dequeueConfiguredReusableCell(using: itemCell, for: indexPath, item: itemIdentifier)
|
|
}
|
|
}
|
|
|
|
private func fetchItems() {
|
|
do {
|
|
let ids = try fervorController.persistentContainer.viewContext.fetch(fetchRequest)
|
|
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, NSManagedObjectID>()
|
|
snapshot.appendSections([.items])
|
|
snapshot.appendItems(ids, toSection: .items)
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
} catch {
|
|
let alert = UIAlertController(title: "Error fetching items", message: error.localizedDescription, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
|
|
present(alert, animated: true)
|
|
}
|
|
}
|
|
|
|
private var updateId = 0
|
|
|
|
@objc private func managedObjectsDidChange(_ notification: Notification) {
|
|
let id = updateId
|
|
updateId += 1
|
|
print("\(id) Managed objects did change")
|
|
let inserted = notification.userInfo?[NSInsertedObjectsKey] as? NSSet
|
|
let updated = notification.userInfo?[NSUpdatedObjectsKey] as? NSSet
|
|
let deleted = notification.userInfo?[NSDeletedObjectsKey] as? NSSet
|
|
|
|
let hasInsertedItems = inserted?.lazy.compactMap { $0 as? Item }.contains { fetchRequest.predicate?.evaluate(with: $0) ?? true } ?? false
|
|
|
|
if hasInsertedItems {
|
|
print("\(id) Has inserted items, skipping merge")
|
|
// if any items were inserted, just refetch everything. it's more expensive than it's worth to try and merge the changes into the current snapshot
|
|
self.fetchItems()
|
|
return
|
|
}
|
|
|
|
var snapshot = self.dataSource.snapshot()
|
|
// the itemIdentifiers getter takes a lot of time in profiles, so only call the getter once
|
|
let snapshotItems = snapshot.itemIdentifiers
|
|
|
|
if let updated = updated {
|
|
print("\(id) Updated: \(updated.count)")
|
|
let knownUpdated = updated.compactMap { ($0 as? Item)?.objectID }.filter { snapshotItems.contains($0) }
|
|
snapshot.reconfigureItems(knownUpdated)
|
|
}
|
|
if let deleted = deleted {
|
|
print("\(id) Deleted: \(deleted.count)")
|
|
let knownDeleted = deleted.compactMap { ($0 as? Item)?.objectID }.filter { snapshotItems.contains($0) }
|
|
snapshot.deleteItems(knownDeleted)
|
|
}
|
|
|
|
print("\(id) Applying snapshot")
|
|
self.dataSource.apply(snapshot, animatingDifferences: false)
|
|
}
|
|
|
|
}
|
|
|
|
extension ItemsViewController {
|
|
enum Section: Hashable {
|
|
case items
|
|
}
|
|
}
|
|
|
|
extension ItemsViewController: UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
guard let id = dataSource.itemIdentifier(for: indexPath) else {
|
|
return nil
|
|
}
|
|
let item = fervorController.persistentContainer.viewContext.object(with: id) as! Item
|
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: {
|
|
ReadViewController(item: item, fervorController: self.fervorController)
|
|
}, actionProvider: { _ in
|
|
var children: [UIMenuElement] = []
|
|
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)
|
|
}))
|
|
#if targetEnvironment(macCatalyst)
|
|
self.activityItemsConfiguration = UIActivityItemsConfiguration(objects: [url as NSURL])
|
|
children.append(UICommand(title: "Share…", action: Selector(("unused")), propertyList: UICommandTagShare))
|
|
#else
|
|
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)
|
|
}))
|
|
#endif
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|