Create SidebarTree. Comment-out SidebarViewController for now.

This commit is contained in:
Brent Simmons
2025-02-07 22:56:55 -08:00
parent 9301bb9961
commit e417f782f9
2 changed files with 469 additions and 133 deletions

View File

@@ -0,0 +1,259 @@
//
// SidebarTree.swift
// NetNewsWire-iOS
//
// Created by Brent Simmons on 2/7/25.
// Copyright © 2025 Ranchero Software. All rights reserved.
//
import Foundation
import Account
typealias SectionID = Int
typealias ItemID = Int
protocol Section: Identifiable {}
protocol Item: Identifiable {}
@MainActor protocol SidebarContainer: Identifiable {
var isExpanded: Bool { get set }
var items: [any Item] { get }
func updateItems()
}
extension SidebarContainer {
func createFeedsDictionary() -> [String: SidebarFeed] {
var d = [String: SidebarFeed]() // feedID: SidebarFeed
for item in items where item is SidebarFeed {
let sidebarFeed = item as! SidebarFeed
d[sidebarFeed.feedID] = sidebarFeed
}
return d
}
func createFoldersDictionary() -> [Int: SidebarFolder] {
var d = [Int: SidebarFolder]()
for item in items where item is SidebarFolder {
let sidebarFolder = item as! SidebarFolder
d[sidebarFolder.folderID] = sidebarFolder
}
return d
}
}
// MARK: - SidebarSmartFeedsFolder
@MainActor final class SidebarSmartFeedsFolder: Section, SidebarContainer {
let id = createID()
var isExpanded = true
let items: [any Item] = [
SidebarSmartFeed(SmartFeedsController.shared.todayFeed),
SidebarSmartFeed(SmartFeedsController.shared.unreadFeed),
SidebarSmartFeed(SmartFeedsController.shared.starredFeed)
]
func updateItems() {
}
}
// MARK: - SidebarSmartFeed
@MainActor final class SidebarSmartFeed: Item {
let id = createID()
let smartFeed: any PseudoFeed
init(_ smartFeed: any PseudoFeed) {
self.smartFeed = smartFeed
}
}
// MARK: - SidebarAccount
@MainActor final class SidebarAccount: Section, SidebarContainer {
let id = createID()
let accountID: String
weak var account: Account?
var isExpanded = true
var items = [any Item]() { // top-level feeds and folders
didSet {
feedsDictionary = createFeedsDictionary()
foldersDictionary = createFoldersDictionary()
}
}
private var feedsDictionary = [String: SidebarFeed]()
private var foldersDictionary = [Int: SidebarFolder]()
init(_ account: Account) {
self.accountID = account.accountID
self.account = account
updateItems()
}
func updateItems() {
guard let account else {
items = [any Item]()
return
}
var sidebarFeeds = [SidebarFeed]()
for feed in account.topLevelFeeds {
if let existingFeed = feedsDictionary[feed.feedID] {
sidebarFeeds.append(existingFeed)
} else {
sidebarFeeds.append(SidebarFeed(feed))
}
}
var sidebarFolders = [SidebarFolder]()
if let folders = account.folders {
for folder in folders {
if let existingFolder = foldersDictionary[folder.folderID] {
sidebarFolders.append(existingFolder)
} else {
sidebarFolders.append(SidebarFolder(folder))
}
}
}
for folder in sidebarFolders {
folder.updateItems()
}
// TODO: sort feeds
items = sidebarFeeds + sidebarFolders
}
}
// MARK: - SidebarFolder
@MainActor final class SidebarFolder: Item, SidebarContainer {
let id = createID()
let folderID: Int
weak var folder: Folder?
var isExpanded = false
var items = [any Item]() { // SidebarFeed
didSet {
feedsDictionary = createFeedsDictionary()
}
}
private var feedsDictionary = [String: SidebarFeed]()
init(_ folder: Folder) {
self.folderID = folder.folderID
self.folder = folder
updateItems()
}
func updateItems() {
guard let folder else {
items = [any Item]()
return
}
var sidebarFeeds = [any Item]()
for feed in folder.topLevelFeeds {
if let existingFeed = feedsDictionary[feed.feedID] {
sidebarFeeds.append(existingFeed)
} else {
sidebarFeeds.append(SidebarFeed(feed))
}
}
// TODO: sort feeds
items = sidebarFeeds
}
}
// MARK: - SidebarFeed
@MainActor final class SidebarFeed: Item {
let id = createID()
let feedID: String
weak var feed: Feed?
init(_ feed: Feed) {
self.feedID = feed.feedID
self.feed = feed
}
}
// MARK: - SidebarTree
@MainActor final class SidebarTree {
var sections = [any Section]()
private lazy var sidebarSmartFeedsFolder = SidebarSmartFeedsFolder()
func rebuildTree() {
rebuildSections()
for section in sections where section is SidebarAccount {
let sidebarAccount = section as! SidebarAccount
sidebarAccount.updateItems()
}
}
}
private extension SidebarTree {
func rebuildSections() {
var updatedSections = [any Section]()
updatedSections.append(sidebarSmartFeedsFolder)
for account in AccountManager.shared.activeAccounts {
if let existingSection = existingSection(for: account) {
updatedSections.append(existingSection)
} else {
updatedSections.append(SidebarAccount(account))
}
}
// TODO: sort accounts
sections = updatedSections
}
func existingSection(for account: Account) -> (any Section)? {
// Linear search through array because its just a few items.
let accountID = account.accountID
for sidebarSection in sections where sidebarSection is SidebarAccount {
let sidebarAccount = sidebarSection as! SidebarAccount
if sidebarAccount.accountID == accountID {
return sidebarAccount
}
}
return nil
}
}
// MARK: - IDs
@MainActor private var autoIncrementingID = 0
@MainActor private func createID() -> Int {
defer { autoIncrementingID += 1 }
return autoIncrementingID
}

View File

@@ -1,136 +1,213 @@
////
//// SidebarViewController.swift
//// NetNewsWire-iOS
////
//// Created by Brent Simmons on 2/2/25.
//// Copyright © 2025 Ranchero Software. All rights reserved.
////
//
// SidebarViewController.swift
// NetNewsWire-iOS
//import Foundation
//import UIKit
//import Account
//
// Created by Brent Simmons on 2/2/25.
// Copyright © 2025 Ranchero Software. All rights reserved.
//final class SidebarViewController: UICollectionViewController {
//
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<Section, SidebarViewController.SidebarItem>
private lazy var dataSource = createDataSource()
private lazy var filterButton = UIBarButtonItem(
image: AppImage.filterInactive,
style: .plain,
target: self,
action: #selector(toggleFilter(_:))
)
private lazy var settingsButton = UIBarButtonItem(
image: AppImage.settings,
style: .plain,
target: self,
action: #selector(showSettings(_:))
)
private lazy var addNewItemButton = UIBarButtonItem(systemItem: .add)
private lazy var refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
private lazy var refreshProgressView: RefreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as! RefreshProgressView
private lazy var flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
private lazy var toolbar = UIToolbar()
init() {
let configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
super.init(collectionViewLayout: layout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0)
title = "Feeds"
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.rightBarButtonItem = filterButton
toolbar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(toolbar)
NSLayoutConstraint.activate([
toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
toolbar.heightAnchor.constraint(equalToConstant: 44)
])
let toolbarItems = [
settingsButton,
flexibleSpaceBarButtonItem,
refreshProgressItemButton,
flexibleSpaceBarButtonItem,
addNewItemButton
]
toolbar.setItems(toolbarItems, animated: false)
applySnapshot()
}
}
// MARK: - Actions
// TODO: Implement actions
extension SidebarViewController {
@objc func toggleFilter(_ sender: Any) {
}
@objc func showSettings(_ sender: Any?) {
}
}
private extension SidebarViewController {
private func createDataSource() -> DataSource {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SidebarItem> { (cell, indexPath, item) in
var content = UIListContentConfiguration.cell()
content.text = item.title
content.image = item.icon
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, SidebarItem>(collectionView: collectionView) { (collectionView, indexPath, item) in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
}
return dataSource
}
private func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, SidebarItem>()
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)
}
}
// typealias DataSource = UICollectionViewDiffableDataSource<SectionID, SidebarItemID>
// private lazy var dataSource = createDataSource()
//
// private lazy var filterButton = UIBarButtonItem(
// image: AppImage.filterInactive,
// style: .plain,
// target: self,
// action: #selector(toggleFilter(_:))
// )
//
// private lazy var settingsButton = UIBarButtonItem(
// image: AppImage.settings,
// style: .plain,
// target: self,
// action: #selector(showSettings(_:))
// )
//
// private lazy var addNewItemButton = UIBarButtonItem(systemItem: .add)
//
// private lazy var refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
// private lazy var refreshProgressView: RefreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as! RefreshProgressView
//
// private lazy var flexibleSpaceBarButtonItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
//
// private lazy var toolbar = UIToolbar()
//
// init() {
// var configuration = UICollectionLayoutListConfiguration(appearance: .sidebar)
// configuration.headerMode = .supplementary
// let layout = UICollectionViewCompositionalLayout.list(using: configuration)
//
// super.init(collectionViewLayout: layout)
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// override func viewDidLoad() {
//
// super.viewDidLoad()
//
// collectionView.contentInset = UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0)
//
// title = "Feeds"
// navigationController?.navigationBar.prefersLargeTitles = true
// navigationItem.rightBarButtonItem = filterButton
//
// toolbar.translatesAutoresizingMaskIntoConstraints = false
// view.addSubview(toolbar)
//
// NSLayoutConstraint.activate([
// toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
// toolbar.heightAnchor.constraint(equalToConstant: 44)
// ])
//
// let toolbarItems = [
// settingsButton,
// flexibleSpaceBarButtonItem,
// refreshProgressItemButton,
// flexibleSpaceBarButtonItem,
// addNewItemButton
// ]
// toolbar.setItems(toolbarItems, animated: false)
//
// collectionView.register(SidebarHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SidebarHeaderView.reuseIdentifier)
//
// applySnapshot()
// }
//}
//
//// MARK: - Actions
//
//// TODO: Implement actions
//
//extension SidebarViewController {
//
// @objc func toggleFilter(_ sender: Any) {
// }
//
// @objc func showSettings(_ sender: Any?) {
// }
//}
//
//// MARK: - SidebarHeaderViewDelegate
//
//extension SidebarViewController: SidebarHeaderViewDelegate {
//
// func sidebarHeaderViewUserDidToggleExpanded(_ sidebarHeaderView: SidebarHeaderView) {
// var section = sections[sidebarHeaderView.sectionIndex]
// section.isExpanded.toggle()
// applySnapshot()
// }
//}
//
//private extension SidebarViewController {
//
// func createDataSource() -> DataSource {
// let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, SidebarItem> { (cell, _, item) in
// var content = UIListContentConfiguration.cell()
// content.text = item.title
// content.image = item.icon
// cell.contentConfiguration = content
// }
//
// dataSource = UICollectionViewDiffableDataSource<Section, SidebarItem>(collectionView: collectionView) { (collectionView, indexPath, item) in
// return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: item)
// }
//
// let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { header, _, indexPath in
//
// let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
//
// var content = UIListContentConfiguration.sidebarHeader()
// content.text = sectionTitle(for: section)
// header.contentConfiguration = content
//
// // Add a disclosure indicator to show collapsible behavior
// let button = UIButton(type: .system)
// button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
// button.tintColor = .secondaryLabel
// button.addTarget(self, action: #selector(self.toggleSection(_:)), for: .touchUpInside)
// button.tag = indexPath.section
// header.accessories = [.customView(configuration: .init(customView: button, placement: .trailing(displayed: .always)))]
//
// // Rotate chevron based on section state
// button.transform = sectionStates[section] == true ? .identity : CGAffineTransform(rotationAngle: -.pi / 2)
// }
//
// dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SidebarHeaderView.reuseIdentifier, for: indexPath) as! SidebarHeaderView
// let section = self.sections[indexPath.section]
// header.configure(title: section.title, sectionIndex: indexPath.section, delegate: self)
// return header
// }
//
// return dataSource
// }
//
// func applySnapshot() {
// var snapshot = NSDiffableDataSourceSnapshot<Section, SidebarItem>()
//
// for section in sections {
// snapshot.appendSections([section])
// if section.isExpanded {
// snapshot.appendItems(section.items, toSection: section)
// }
// }
//
// dataSource.apply(snapshot, animatingDifferences: true)
// }
//}
//
//protocol SidebarHeaderViewDelegate: AnyObject {
//
// func sidebarHeaderViewUserDidToggleExpanded(_: SidebarHeaderView)
//}
//
//final class SidebarHeaderView: UICollectionReusableView {
//
// static let reuseIdentifier = "SidebarHeaderView"
//
// var sectionIndex: Int = 0
// var delegate: SidebarHeaderViewDelegate?
//
// override init(frame: CGRect) {
// super.init(frame: frame)
// backgroundColor = .darkGray
//
// let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleExpanded(_:)))
// addGestureRecognizer(tapGesture)
// }
//
// required init?(coder: NSCoder) {
// fatalError("init(coder:) has not been implemented")
// }
//
// func configure(title: String, sectionIndex: Int, delegate: SidebarHeaderViewDelegate) {
// self.sectionIndex = sectionIndex
// self.delegate = delegate
//
// let label = UILabel(frame: bounds)
// label.text = title
// label.textColor = .white
// label.textAlignment = .center
// addSubview(label)
// }
//
// @objc func toggleExpanded(_ sender: Any?) {
// delegate?.sidebarHeaderViewUserDidToggleExpanded(self)
//// guard let controller else {
//// return
//// }
//// controller.sections[sectionIndex].state.toggle()
//// controller.applySnapshot()
// }
//}