From 85f25556f9a66e686cf704640037e8f960795faa Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 2 Feb 2025 21:43:10 -0800 Subject: [PATCH] Create SidebarViewController, a UICollectionViewController subclass, to replace the UITableViewController subclass previously in use. This will get us the modern sidebar appearance. --- Shared/AppImage.swift | 20 ++--- iOS/MainWindow/MainWindowController.swift | 2 +- iOS/MainWindow/RootSplitViewController.swift | 6 +- iOS/MainWindow/SceneCoordinator.swift | 22 +++--- .../Sidebar/SidebarViewController.swift | 77 +++++++++++++++++++ 5 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 iOS/MainWindow/Sidebar/SidebarViewController.swift diff --git a/Shared/AppImage.swift b/Shared/AppImage.swift index e5ea40968..68d0ff05f 100644 --- a/Shared/AppImage.swift +++ b/Shared/AppImage.swift @@ -136,6 +136,7 @@ extension AppImage { extension AppImage { #if os(iOS) + static var allUnread = systemImage("largecircle.fill.circle") static var articleExtractorOffSF = systemImage("doc.plaintext") static var articleExtractorOnSF = appImage("articleExtractorOnSF") static var articleExtractorOffTinted = articleExtractorOff.tinted(color: AppColor.accent)! @@ -158,7 +159,9 @@ extension AppImage { static var previousArticle = systemImage("chevron.up") static var safari = systemImage("safari") static var settings = systemImage("gear") + static var starred = systemImage("star.fill") static var timelineStar = systemImage("star.fill").withTintColor(AppColor.star, renderingMode: .alwaysOriginal) + static var today = systemImage("sun.max.fill") static var trash = systemImage("trash") // IconImages @@ -167,20 +170,9 @@ extension AppImage { // TODO: handle color palette change - static var starredFeed: IconImage = { - let image = systemImage("star.fill") - return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.star.cgColor) - }() - - static var todayFeed: IconImage = { - let image = systemImage("sun.max.fill") - return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor) - }() - - static var unreadFeed: IconImage = { - let image = systemImage("largecircle.fill.circle") - return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.secondaryAccent.cgColor) - }() + static var starredFeed = IconImage(starred, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.star.cgColor) + static var todayFeed = IconImage(today, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor) + static var unreadFeed = IconImage(allUnread, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppColor.secondaryAccent.cgColor) static var folder: IconImage = { let image = systemImage("folder.fill") diff --git a/iOS/MainWindow/MainWindowController.swift b/iOS/MainWindow/MainWindowController.swift index 7aba3fe91..38eb5cd45 100644 --- a/iOS/MainWindow/MainWindowController.swift +++ b/iOS/MainWindow/MainWindowController.swift @@ -14,7 +14,7 @@ final class MainWindowController { let window: UIWindow let rootSplitViewController: RootSplitViewController - let sidebarViewController = MainFeedViewController() + let sidebarViewController = SidebarViewController() let timelineViewController = TimelineViewController() let articleViewController = ArticleViewController() diff --git a/iOS/MainWindow/RootSplitViewController.swift b/iOS/MainWindow/RootSplitViewController.swift index 945168d97..14bef72ff 100644 --- a/iOS/MainWindow/RootSplitViewController.swift +++ b/iOS/MainWindow/RootSplitViewController.swift @@ -13,17 +13,17 @@ final class RootSplitViewController: UISplitViewController { var coordinator: SceneCoordinator! { didSet { - sidebarViewController.coordinator = coordinator +// sidebarViewController.coordinator = coordinator timelineViewController.coordinator = coordinator articleViewController.coordinator = coordinator } } - private let sidebarViewController: MainFeedViewController + private let sidebarViewController: SidebarViewController private let timelineViewController: TimelineViewController private let articleViewController: ArticleViewController - init(sidebarViewController: MainFeedViewController, + init(sidebarViewController: SidebarViewController, timelineViewController: TimelineViewController, articleViewController: ArticleViewController) { diff --git a/iOS/MainWindow/SceneCoordinator.swift b/iOS/MainWindow/SceneCoordinator.swift index 5b174b690..87310795d 100644 --- a/iOS/MainWindow/SceneCoordinator.swift +++ b/iOS/MainWindow/SceneCoordinator.swift @@ -50,7 +50,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { private var rootSplitViewController: RootSplitViewController! - private var mainFeedViewController: MainFeedViewController! + private var mainFeedViewController: MainFeedViewController? private var mainTimelineViewController: TimelineViewController? private var articleViewController: ArticleViewController? @@ -279,7 +279,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { super.init() self.mainFeedViewController = rootSplitViewController.viewController(for: .primary) as? MainFeedViewController - self.mainFeedViewController.coordinator = self + self.mainFeedViewController?.coordinator = self self.mainFeedViewController?.navigationController?.delegate = self self.mainTimelineViewController = rootSplitViewController.viewController(for: .supplementary) as? TimelineViewController @@ -741,7 +741,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { } currentFeedIndexPath = indexPath - mainFeedViewController.updateFeedSelection(animations: animations) + mainFeedViewController?.updateFeedSelection(animations: animations) if deselectArticle { selectArticle(nil) @@ -1168,14 +1168,14 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { addNavViewController.modalPresentationStyle = .formSheet addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay - mainFeedViewController.present(addNavViewController, animated: true) + mainFeedViewController?.present(addNavViewController, animated: true) } func showAddFolder() { let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFolderViewControllerNav") as! UINavigationController addNavViewController.modalPresentationStyle = .formSheet addNavViewController.preferredContentSize = AddFolderViewController.preferredContentSizeForFormSheetDisplay - mainFeedViewController.present(addNavViewController, animated: true) + mainFeedViewController?.present(addNavViewController, animated: true) } func showFullScreenImage(image: UIImage, imageTitle: String?, transitioningDelegate: UIViewControllerTransitioningDelegate) { @@ -1223,7 +1223,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { if currentArticle != nil { articleViewController?.openInAppBrowser() } else { - mainFeedViewController.openInAppBrowser() + mainFeedViewController?.openInAppBrowser() } } @@ -1270,7 +1270,7 @@ final class SceneCoordinator: NSObject, UndoableCommandRunner { /// `SFSafariViewController` or `SettingsViewController`, /// otherwise, this function does nothing. func dismissIfLaunchingFromExternalAction() { - guard let presentedController = mainFeedViewController.presentedViewController else { return } + guard let presentedController = mainFeedViewController?.presentedViewController else { return } if presentedController.isKind(of: SFSafariViewController.self) { presentedController.dismiss(animated: true, completion: nil) @@ -1462,7 +1462,7 @@ private extension SceneCoordinator { updateExpandedNodes?() let changes = rebuildShadowTable() - mainFeedViewController.reloadFeeds(initialLoad: initialLoad, changes: changes, completion: completion) + mainFeedViewController?.reloadFeeds(initialLoad: initialLoad, changes: changes, completion: completion) } } @@ -2072,7 +2072,7 @@ private extension SceneCoordinator { self.treeControllerDelegate.resetFilterExceptions() if let indexPath = self.indexPathFor(smartFeed) { self.selectFeed(indexPath: indexPath) { - self.mainFeedViewController.focus() + self.mainFeedViewController?.focus() } } }) @@ -2093,7 +2093,7 @@ private extension SceneCoordinator { if let folderNode = self.findFolderNode(folderName: folderName, beginningAt: accountNode), let indexPath = self.indexPathFor(folderNode) { self.selectFeed(indexPath: indexPath) { - self.mainFeedViewController.focus() + self.mainFeedViewController?.focus() } } }) @@ -2106,7 +2106,7 @@ private extension SceneCoordinator { } self.discloseFeed(feed, initialLoad: true) { - self.mainFeedViewController.focus() + self.mainFeedViewController?.focus() } } } diff --git a/iOS/MainWindow/Sidebar/SidebarViewController.swift b/iOS/MainWindow/Sidebar/SidebarViewController.swift new file mode 100644 index 000000000..49e973979 --- /dev/null +++ b/iOS/MainWindow/Sidebar/SidebarViewController.swift @@ -0,0 +1,77 @@ +// +// SidebarViewController.swift +// NetNewsWire-iOS +// +// Created by Brent Simmons on 2/2/25. +// Copyright © 2025 Ranchero Software. All rights reserved. +// + +import Foundation +import UIKit + +final class SidebarViewController: UICollectionViewController { + + enum Section { + case smartFeeds + } + + struct SidebarItem: Hashable, Identifiable { + let id: UUID = UUID() + let title: String + let icon: UIImage? + } + + typealias DataSource = UICollectionViewDiffableDataSource + private lazy var dataSource = createDataSource() + + init() { + super.init(collectionViewLayout: Self.createSidebarLayout()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + + super.viewDidLoad() + + applySnapshot() + } +} + +private extension SidebarViewController { + + static func createSidebarLayout() -> UICollectionViewLayout { + let configuration = UICollectionLayoutListConfiguration(appearance: .sidebar) + return UICollectionViewCompositionalLayout.list(using: configuration) + } + + private func createDataSource() -> DataSource { + let cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, item) in + var content = UIListContentConfiguration.cell() + content.text = item.title + content.image = item.icon + cell.contentConfiguration = content + } + + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) in + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item) + } + + return dataSource + } + + private func applySnapshot() { + var snapshot = NSDiffableDataSourceSnapshot() + + snapshot.appendSections([.smartFeeds]) + snapshot.appendItems([ + SidebarItem(title: "Today", icon: AppImage.today), + SidebarItem(title: "All Unread", icon: AppImage.allUnread), + SidebarItem(title: "Starred", icon: AppImage.starred) + ]) + + dataSource.apply(snapshot, animatingDifferences: true) + } +}