// // FeedDetailViewController.swift // NewsBlur // // Created by David Sinclair on 2020-08-27. // Copyright © 2020 NewsBlur. All rights reserved. // import UIKit import SwiftUI /// List of stories for a feed. class FeedDetailViewController: FeedDetailObjCViewController { lazy var gridViewController = makeGridViewController() lazy var storyCache = StoryCache() enum SectionLayoutKind: Int, CaseIterable { /// Feed cells before the story. case feedBeforeStory /// The selected story. case selectedStory /// Feed cells after the story. case feedAfterStory /// Loading cell at the end. case loading } var isGrid: Bool { return appDelegate.detailViewController.layout == .grid } var feedColumns: Int { guard let pref = UserDefaults.standard.string(forKey: "grid_columns"), let columns = Int(pref) else { return 4 } return columns } var gridHeight: CGFloat { guard let pref = UserDefaults.standard.string(forKey: "grid_height") else { return 400 } switch pref { case "xs": return 250 case "short": return 300 case "tall": return 400 case "xl": return 450 default: return 350 } } // var storyHeight: CGFloat { // if let pagesController = appDelegate.storyPagesViewController, let webView = pagesController.currentPage.webView { // let frame = pagesController.view.frame // // print("Story pages frame: \(pagesController.view.frame), web height \(webView.scrollView.contentSize.height)") // // pagesController.view.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: 500) // pagesController.currentPage.view.frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: 500) // pagesController.view.layoutIfNeeded() // // let height = webView.scrollView.contentSize.height + 50 // // print("... frame now: \(pagesController.view.frame), height: \(height)") // // return height // } else { // return 1000 // } // } var dataSource: UICollectionViewDiffableDataSource! = nil private func makeGridViewController() -> UIHostingController { // let headerView = FeedDetailGridView(isGrid: isGrid, storyCache: storyCache) let gridView = FeedDetailGridView(feedDetailInteraction: self, cache: storyCache) let gridViewController = UIHostingController(rootView: gridView) gridViewController.view.translatesAutoresizingMaskIntoConstraints = false return gridViewController } override func viewDidLoad() { super.viewDidLoad() changedLayout() configureDataSource() feedCollectionView.isHidden = true addChild(gridViewController) view.addSubview(gridViewController.view) gridViewController.didMove(toParent: self) NSLayoutConstraint.activate([ gridViewController.view.topAnchor.constraint(equalTo: view.topAnchor), gridViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), gridViewController.view.widthAnchor.constraint(equalTo: view.widthAnchor), gridViewController.view.heightAnchor.constraint(equalTo: view.heightAnchor) ]) } @objc override func changedLayout() { if isGrid { feedCollectionView.collectionViewLayout = createGridLayout() } else { feedCollectionView.collectionViewLayout = createListLayout() } feedCollectionView.setNeedsLayout() } @objc override func reload() { configureDataSource() } @objc override func reload(_ indexPath: IndexPath) { configureDataSource() } } extension FeedDetailViewController { func accessoriesForListCellItem(_ item: Int) -> [UICellAccessory] { let isStarred = false //self.starredEmojis.contains(item) var accessories = [UICellAccessory.disclosureIndicator()] if isStarred { let star = UIImageView(image: UIImage(systemName: "star.fill")) accessories.append(.customView(configuration: .init(customView: star, placement: .trailing()))) } return accessories } func leadingSwipeActionConfigurationForListCellItem(_ item: Int) -> UISwipeActionsConfiguration? { let isStarred = false //self.starredEmojis.contains(item) let starAction = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completion) in guard let self = self else { completion(false) return } // Don't check again for the starred state. We promised in the UI what this action will do. // If the starred state has changed by now, we do nothing, as the set will not change. // if isStarred { // self.starredEmojis.remove(item) // } else { // self.starredEmojis.insert(item) // } // Reconfigure the cell of this item // Make sure we get the current index path of the item. if let currentIndexPath = self.dataSource.indexPath(for: item) { if let cell = self.feedCollectionView.cellForItem(at: currentIndexPath) as? UICollectionViewListCell { UIView.animate(withDuration: 0.2) { cell.accessories = self.accessoriesForListCellItem(item) } } } completion(true) } starAction.image = UIImage(systemName: isStarred ? "star.slash" : "star.fill") starAction.backgroundColor = .systemBlue return UISwipeActionsConfiguration(actions: [starAction]) } func createListLayout() -> UICollectionViewLayout { // let size = NSCollectionLayoutSize( // widthDimension: NSCollectionLayoutDimension.fractionalWidth(1), // heightDimension: NSCollectionLayoutDimension.estimated(200) // ) // let item = NSCollectionLayoutItem(layoutSize: size) // let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: 1) // // let section = NSCollectionLayoutSection(group: group) // section.interGroupSpacing = 0 // // return UICollectionViewCompositionalLayout(section: section) var configuration = UICollectionLayoutListConfiguration(appearance: .plain) configuration.leadingSwipeActionsConfigurationProvider = { [weak self] (indexPath) in guard let self else { return nil } guard let item = self.dataSource.itemIdentifier(for: indexPath) else { return nil } return self.leadingSwipeActionConfigurationForListCellItem(item) } return UICollectionViewCompositionalLayout.list(using: configuration) } func createGridLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in guard let sectionLayoutKind = SectionLayoutKind(rawValue: sectionIndex) else { return nil } let isStory = sectionLayoutKind == .selectedStory let isLoading = sectionLayoutKind == .loading let columns = isStory || isLoading ? 1 : self.feedColumns let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) let groupHeight = isStory ? NSCollectionLayoutDimension.absolute(self.storyHeight) : isLoading ? NSCollectionLayoutDimension.absolute(100) : NSCollectionLayoutDimension.absolute(self.gridHeight) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: groupHeight) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: columns) let section = NSCollectionLayoutSection(group: group) section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 10) return section } return layout } } extension FeedDetailViewController { func configureDataSource() { let feedCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, identifier) in // cell.frame.size.height = self.heightForRow(at: indexPath) self.prepareFeedCell(cell, indexPath: indexPath) cell.setNeedsUpdateConfiguration() } let storyCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, identifier) in self.prepareStoryCell(cell, indexPath: indexPath) cell.setNeedsUpdateConfiguration() } let loadingCellRegistration = UICollectionView.CellRegistration { (cell, indexPath, identifier) in self.prepareLoading(cell, indexPath: indexPath) cell.setNeedsUpdateConfiguration() } dataSource = UICollectionViewDiffableDataSource(collectionView: feedCollectionView) { (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in guard let sectionKind = SectionLayoutKind(rawValue: indexPath.section) else { return nil } switch sectionKind { case .feedBeforeStory, .feedAfterStory: return collectionView.dequeueConfiguredReusableCell(using: feedCellRegistration, for: indexPath, item: identifier) case .selectedStory: if self.isGrid { return collectionView.dequeueConfiguredReusableCell(using: storyCellRegistration, for: indexPath, item: identifier) } else { return collectionView.dequeueConfiguredReusableCell(using: feedCellRegistration, for: indexPath, item: identifier) } case .loading: return collectionView.dequeueConfiguredReusableCell(using: loadingCellRegistration, for: indexPath, item: identifier) } } var snapshot = NSDiffableDataSourceSnapshot() let storyCount = Int(appDelegate.storiesCollection.storyLocationsCount) var beforeSelection = [Int]() var selectedIndex = -999 var afterSelection = [Int]() snapshot.appendSections(SectionLayoutKind.allCases) if self.messageView.isHidden { if storyCount > 0 { selectedIndex = appDelegate.storiesCollection.indexOfActiveStory() if selectedIndex < 0 { beforeSelection = Array(0..= storyCache.before.count + storyCache.after.count - 5 { if storiesCollection.isRiverView, storiesCollection.activeFolder != nil { fetchRiverPage(storiesCollection.feedPage + 1, withCallback: nil) } else { fetchFeedDetail(storiesCollection.feedPage + 1, withCallback: nil) } } } func storyTapped(_ story: Story) { print("tapped \(story.title)") let indexPath = IndexPath(row: story.index, section: 0) //TODO: 🚧 change this function to work better with the grid view collectionView(feedCollectionView, didSelectItemAt: indexPath) } func storyHidden(_ story: Story) { print("hiding \(story.title)") let indexPath = IndexPath(row: story.index, section: 0) appDelegate.activeStory = nil reload() } }