From e81defb934d5e9a990ae1ee02df384263ec989ad Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 21 Apr 2019 17:42:26 -0500 Subject: [PATCH] Implement detail next and prev article buttons. --- .../Detail/DetailViewController.swift | 2 + NetNewsWire.xcodeproj/project.pbxproj | 8 +- iOS/AppDelegate.swift | 2 +- iOS/Base.lproj/Main.storyboard | 21 ++++- iOS/Detail/DetailViewController.swift | 54 ++++++++---- iOS/Master/MasterViewController.swift | 84 +++++++++---------- ....swift => NavigationStateController.swift} | 79 +++++++++++++---- .../MasterTimelineViewController.swift | 76 ++++++++++------- 8 files changed, 216 insertions(+), 110 deletions(-) rename iOS/{NavigationModelController.swift => NavigationStateController.swift} (87%) diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index a7a8ce63a..6a1b11e94 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -14,6 +14,8 @@ import RSWeb enum DetailState: Equatable { case noSelection + @IBOutlet weak var nextUnreadButtonItem: UIBarButtonItem! + @IBOutlet weak var nextUnreadButtonItem: UIBarButtonItem! case multipleSelection case article(Article) } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 11972cec0..7591fcb9a 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; - 5126EE97226CB48A00C22AFC /* NavigationModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationModelController.swift */; }; + 5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */; }; 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; @@ -603,7 +603,7 @@ 51121AA12265430A00BC0EC1 /* NetNewsWire_iOS_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOS_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; - 5126EE96226CB48A00C22AFC /* NavigationModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModelController.swift; sourceTree = ""; }; + 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStateController.swift; sourceTree = ""; }; 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = ""; }; 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = ""; }; 512E08F722688F7C00BDCFDD /* MasterTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTableViewSectionHeader.swift; sourceTree = ""; }; @@ -1514,7 +1514,7 @@ 840D617E2029031C009BC708 /* AppDelegate.swift */, 51C45254226507D200C03939 /* AppAssets.swift */, 51C45255226507D200C03939 /* AppDefaults.swift */, - 5126EE96226CB48A00C22AFC /* NavigationModelController.swift */, + 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */, 51C4525D226508F600C03939 /* Master */, 51C4526D2265091600C03939 /* Timeline */, 51C4527D2265092C00C03939 /* Detail */, @@ -2149,7 +2149,7 @@ 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterViewController.swift in Sources */, - 5126EE97226CB48A00C22AFC /* NavigationModelController.swift in Sources */, + 5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index fdba742a3..500089111 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -128,7 +128,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool { guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false } guard let topAsDetailController = secondaryAsNavController.topViewController as? DetailViewController else { return false } - if topAsDetailController.article == nil { + if topAsDetailController.navState?.currentArticle == nil { // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. return true } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 014c8262c..ccccd140a 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -57,11 +57,23 @@ - + + + + + - + + + + + - + + + + + @@ -92,6 +104,9 @@ + + + diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Detail/DetailViewController.swift index 63373c05c..d2f64290b 100644 --- a/iOS/Detail/DetailViewController.swift +++ b/iOS/Detail/DetailViewController.swift @@ -14,18 +14,16 @@ import SafariServices class DetailViewController: UIViewController { + @IBOutlet weak var nextUnreadBarButtonItem: UIBarButtonItem! + @IBOutlet weak var prevArticleBarButtonItem: UIBarButtonItem! + @IBOutlet weak var nextArticleBarButtonItem: UIBarButtonItem! @IBOutlet weak var readBarButtonItem: UIBarButtonItem! @IBOutlet weak var starBarButtonItem: UIBarButtonItem! @IBOutlet weak var actionBarButtonItem: UIBarButtonItem! @IBOutlet weak var browserBarButtonItem: UIBarButtonItem! @IBOutlet weak var webView: WKWebView! - var article: Article? { - didSet { - reloadUI() - reloadHTML() - } - } + weak var navState: NavigationStateController? override func viewDidLoad() { super.viewDidLoad() @@ -34,11 +32,15 @@ class DetailViewController: UIViewController { reloadUI() reloadHTML() NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionChange(_:)), name: .ArticleSelectionChange, object: navState) } func reloadUI() { - guard let article = article else { + guard let article = navState?.currentArticle else { + nextUnreadBarButtonItem.isEnabled = false + prevArticleBarButtonItem.isEnabled = false + nextArticleBarButtonItem.isEnabled = false readBarButtonItem.isEnabled = false starBarButtonItem.isEnabled = false browserBarButtonItem.isEnabled = false @@ -46,6 +48,10 @@ class DetailViewController: UIViewController { return } + nextArticleBarButtonItem.isEnabled = false + prevArticleBarButtonItem.isEnabled = navState?.isPrevArticleAvailable ?? false + nextArticleBarButtonItem.isEnabled = navState?.isNextArticleAvailable ?? false + readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true browserBarButtonItem.isEnabled = true @@ -60,47 +66,67 @@ class DetailViewController: UIViewController { } func reloadHTML() { - guard let article = article, let webView = webView else { + + guard let article = navState?.currentArticle, let webView = webView else { return } let style = ArticleStylesManager.shared.currentStyle let html = ArticleRenderer.articleHTML(article: article, style: style) webView.loadHTMLString(html, baseURL: nil) + } @objc func statusesDidChange(_ note: Notification) { guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set
else { return } - if articles.count == 1 && articles.first?.articleID == article?.articleID { + if articles.count == 1 && articles.first?.articleID == navState?.currentArticle?.articleID { reloadUI() } } + @objc func articleSelectionChange(_ note: Notification) { + reloadUI() + reloadHTML() + } + + // MARK: Actions + + @IBAction func nextUnread(_ sender: Any) { + } + + @IBAction func prevArticle(_ sender: Any) { + navState?.currentArticleIndexPath = navState?.prevArticleIndexPath + } + + @IBAction func nextArticle(_ sender: Any) { + navState?.currentArticleIndexPath = navState?.nextArticleIndexPath + } + @IBAction func toggleRead(_ sender: Any) { - if let article = article { + if let article = navState?.currentArticle { markArticles(Set([article]), statusKey: .read, flag: !article.status.read) } } @IBAction func toggleStar(_ sender: Any) { - if let article = article { + if let article = navState?.currentArticle { markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred) } } @IBAction func openBrowser(_ sender: Any) { - guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { + guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else { return } UIApplication.shared.open(url, options: [:]) } @IBAction func showActivityDialog(_ sender: Any) { - guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else { + guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else { return } - let itemSource = ArticleActivityItemSource(url: url, subject: article?.title) + let itemSource = ArticleActivityItemSource(url: url, subject: navState?.currentArticle?.title) let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil) present(activityViewController, animated: true) } diff --git a/iOS/Master/MasterViewController.swift b/iOS/Master/MasterViewController.swift index 162bff11e..a9c9d4080 100644 --- a/iOS/Master/MasterViewController.swift +++ b/iOS/Master/MasterViewController.swift @@ -16,7 +16,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { var undoableCommands = [UndoableCommand]() - let nmc = NavigationModelController() + let navState = NavigationStateController() override var canBecomeFirstResponder: Bool { return true } @@ -77,8 +77,8 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { } if let account = representedObject as? Account { - if let node = nmc.rootNode.childNodeRepresentingObject(account) { - let sectionIndex = nmc.rootNode.indexOfChild(node)! + if let node = navState.rootNode.childNodeRepresentingObject(account) { + let sectionIndex = navState.rootNode.indexOfChild(node)! let headerView = tableView.headerView(forSection: sectionIndex) as! MasterTableViewSectionHeader headerView.unreadCount = account.unreadCount } @@ -108,27 +108,27 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { @objc func userDidAddFeed(_ notification: Notification) { guard let feed = notification.userInfo?[UserInfoKey.feed], - let node = nmc.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else { + let node = navState.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else { return } - if let indexPath = nmc.indexPathFor(node) { + if let indexPath = navState.indexPathFor(node) { tableView.scrollToRow(at: indexPath, at: .middle, animated: true) return } // It wasn't already visable, so expand its folder and try again - guard let parent = node.parent, let indexPath = nmc.indexPathFor(parent) else { + guard let parent = node.parent, let indexPath = navState.indexPathFor(parent) else { return } - nmc.expand(indexPath) { [weak self] indexPaths in + navState.expand(indexPath) { [weak self] indexPaths in self?.tableView.beginUpdates() self?.tableView.insertRows(at: indexPaths, with: .automatic) self?.tableView.endUpdates() } - if let indexPath = nmc.indexPathFor(node) { + if let indexPath = navState.indexPathFor(node) { tableView.scrollToRow(at: indexPath, at: .middle, animated: true) } @@ -137,11 +137,11 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { // MARK: Table View override func numberOfSections(in tableView: UITableView) -> Int { - return nmc.shadowTable.count + return navState.shadowTable.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return nmc.shadowTable[section].count + return navState.shadowTable[section].count } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { @@ -150,14 +150,14 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let nameProvider = nmc.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else { + guard let nameProvider = navState.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else { return nil } let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! MasterTableViewSectionHeader headerView.name = nameProvider.nameForDisplay - guard let sectionNode = nmc.rootNode.childAtIndex(section) else { + guard let sectionNode = navState.rootNode.childAtIndex(section) else { return headerView } @@ -168,7 +168,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { } headerView.tag = section - headerView.disclosureExpanded = nmc.expandedNodes.contains(sectionNode) + headerView.disclosureExpanded = navState.expandedNodes.contains(sectionNode) let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:))) headerView.addGestureRecognizer(tap) @@ -189,7 +189,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTableViewCell - guard let node = nmc.nodeFor(indexPath) else { + guard let node = navState.nodeFor(indexPath) else { return cell } @@ -199,7 +199,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - guard let node = nmc.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else { + guard let node = navState.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else { return false } return true @@ -231,7 +231,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let node = nmc.nodeFor(indexPath) else { + guard let node = navState.nodeFor(indexPath) else { assertionFailure() return } @@ -239,21 +239,21 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) if let fetcher = node.representedObject as? ArticleFetcher { - nmc.timelineFetcher = fetcher + navState.timelineFetcher = fetcher } if let nameProvider = node.representedObject as? DisplayNameProvider { timeline.title = nameProvider.nameForDisplay } - timeline.nmc = nmc + timeline.navState = navState self.navigationController?.pushViewController(timeline, animated: true) } override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - guard let node = nmc.nodeFor(indexPath) else { + guard let node = navState.nodeFor(indexPath) else { return false } return node.representedObject is Feed @@ -269,13 +269,13 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { return proposedDestinationIndexPath }() - guard let draggedNode = nmc.nodeFor(sourceIndexPath), let destNode = nmc.nodeFor(destIndexPath), let parentNode = destNode.parent else { + guard let draggedNode = navState.nodeFor(sourceIndexPath), let destNode = navState.nodeFor(destIndexPath), let parentNode = destNode.parent else { assertionFailure("This should never happen") return sourceIndexPath } // If this is a folder and isn't expanded or doesn't have any entries, let the users drop on it - if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !nmc.expandedNodes.contains(destNode)) { + if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !navState.expandedNodes.contains(destNode)) { let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0 return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section) } @@ -296,7 +296,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { if parentNode.representedObject is Account { return IndexPath(row: 0, section: destIndexPath.section) } else { - return nmc.indexPathFor(parentNode)! + return navState.indexPathFor(parentNode)! } } else { @@ -306,10 +306,10 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 let adjustedIndex = index - movementAdjustment if adjustedIndex >= sortedNodes.count { - let lastSortedIndexPath = nmc.indexPathFor(sortedNodes[sortedNodes.count - 1])! + let lastSortedIndexPath = navState.indexPathFor(sortedNodes[sortedNodes.count - 1])! return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section) } else { - return nmc.indexPathFor(sortedNodes[adjustedIndex])! + return navState.indexPathFor(sortedNodes[adjustedIndex])! } } @@ -318,18 +318,18 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - guard let sourceNode = nmc.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else { + guard let sourceNode = navState.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else { return } // Based on the drop we have to determine a node to start looking for a parent container. let destNode: Node = { if destinationIndexPath.row == 0 { - return nmc.rootNode.childAtIndex(destinationIndexPath.section)! + return navState.rootNode.childAtIndex(destinationIndexPath.section)! } else { let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0 let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section) - return nmc.nodeFor(adjustedDestIndexPath)! + return navState.nodeFor(adjustedDestIndexPath)! } }() @@ -452,22 +452,22 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { @objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) { guard let sectionIndex = sender.view?.tag, - let sectionNode = nmc.rootNode.childAtIndex(sectionIndex), + let sectionNode = navState.rootNode.childAtIndex(sectionIndex), let headerView = sender.view as? MasterTableViewSectionHeader else { return } - if nmc.expandedNodes.contains(sectionNode) { + if navState.expandedNodes.contains(sectionNode) { headerView.disclosureExpanded = false - nmc.collapse(section: sectionIndex) { [weak self] indexPaths in + navState.collapse(section: sectionIndex) { [weak self] indexPaths in self?.tableView.beginUpdates() self?.tableView.deleteRows(at: indexPaths, with: .automatic) self?.tableView.endUpdates() } } else { headerView.disclosureExpanded = true - nmc.expand(section: sectionIndex) { [weak self] indexPaths in + navState.expand(section: sectionIndex) { [weak self] indexPaths in self?.tableView.beginUpdates() self?.tableView.insertRows(at: indexPaths, with: .automatic) self?.tableView.endUpdates() @@ -486,7 +486,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { } else { cell.indentationLevel = 0 } - cell.disclosureExpanded = nmc.expandedNodes.contains(node) + cell.disclosureExpanded = navState.expandedNodes.contains(node) cell.allowDisclosureSelection = node.canHaveChildNodes cell.name = nameFor(node) @@ -528,25 +528,25 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { func delete(indexPath: IndexPath) { guard let undoManager = undoManager, - let deleteNode = nmc.nodeFor(indexPath), - let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: nmc.treeController, undoManager: undoManager) + let deleteNode = navState.nodeFor(indexPath), + let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: navState.treeController, undoManager: undoManager) else { return } - nmc.animatingChanges = true + navState.animatingChanges = true runCommand(deleteCommand) - nmc.rebuildShadowTable() + navState.rebuildShadowTable() tableView.deleteRows(at: [indexPath], with: .automatic) - nmc.animatingChanges = false + navState.animatingChanges = false } func rename(indexPath: IndexPath) { - let name = (nmc.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? "" + let name = (navState.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? "" let formatString = NSLocalizedString("Rename “%@”", comment: "Feed finder") let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String @@ -558,7 +558,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner { let renameTitle = NSLocalizedString("Rename", comment: "Rename") let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in - guard let node = self?.nmc.nodeFor(indexPath), + guard let node = self?.navState.nodeFor(indexPath), let name = alertController.textFields?[0].text, !name.isEmpty else { return @@ -645,7 +645,7 @@ private extension MasterViewController { func applyToAvailableCells(_ callback: (MasterTableViewCell, Node) -> Void) { tableView.visibleCells.forEach { cell in - guard let indexPath = tableView.indexPath(for: cell), let node = nmc.nodeFor(indexPath) else { + guard let indexPath = tableView.indexPath(for: cell), let node = navState.nodeFor(indexPath) else { return } callback(cell as! MasterTableViewCell, node) @@ -669,7 +669,7 @@ private extension MasterViewController { guard let indexPath = tableView.indexPath(for: cell) else { return } - nmc.expand(indexPath) { [weak self] indexPaths in + navState.expand(indexPath) { [weak self] indexPaths in self?.tableView.beginUpdates() self?.tableView.insertRows(at: indexPaths, with: .automatic) self?.tableView.endUpdates() @@ -680,7 +680,7 @@ private extension MasterViewController { guard let indexPath = tableView.indexPath(for: cell) else { return } - nmc.collapse(indexPath) { [weak self] indexPaths in + navState.collapse(indexPath) { [weak self] indexPaths in self?.tableView.beginUpdates() self?.tableView.deleteRows(at: indexPaths, with: .automatic) self?.tableView.endUpdates() diff --git a/iOS/NavigationModelController.swift b/iOS/NavigationStateController.swift similarity index 87% rename from iOS/NavigationModelController.swift rename to iOS/NavigationStateController.swift index d024d23ea..e6d29aa69 100644 --- a/iOS/NavigationModelController.swift +++ b/iOS/NavigationStateController.swift @@ -18,25 +18,20 @@ public extension Notification.Name { static let ArticlesReinitialized = Notification.Name(rawValue: "ArticlesReinitialized") static let ArticleDataDidChange = Notification.Name(rawValue: "ArticleDataDidChange") static let ArticlesDidChange = Notification.Name(rawValue: "ArticlesDidChange") + static let ArticleSelectionChange = Notification.Name(rawValue: "ArticleSelectionChange") } -class NavigationModelController { +class NavigationStateController { static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) + private var articleRowMap = [String: Int]() // articleID: rowIndex + + // Eventually I want these to be private too -Maurice var animatingChanges = false var expandedNodes = [Node]() var shadowTable = [[Node]]() - let treeControllerDelegate = FeedTreeControllerDelegate() - lazy var treeController: TreeController = { - return TreeController(delegate: treeControllerDelegate) - }() - - var rootNode: Node { - return treeController.rootNode - } - private var sortDirection = AppDefaults.timelineSortDirection { didSet { if sortDirection != oldValue { @@ -44,6 +39,15 @@ class NavigationModelController { } } } + + private let treeControllerDelegate = FeedTreeControllerDelegate() + lazy var treeController: TreeController = { + return TreeController(delegate: treeControllerDelegate) + }() + + var rootNode: Node { + return treeController.rootNode + } var showFeedNames = false { didSet { @@ -54,6 +58,7 @@ class NavigationModelController { var timelineFetcher: ArticleFetcher? { didSet { + currentArticleIndexPath = nil if timelineFetcher is Feed { showFeedNames = false } else { @@ -63,7 +68,50 @@ class NavigationModelController { NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil) } } - + + var isPrevArticleAvailable: Bool { + guard let indexPath = currentArticleIndexPath else { + return false + } + return indexPath.row > 0 + } + + var isNextArticleAvailable: Bool { + guard let indexPath = currentArticleIndexPath else { + return false + } + return indexPath.row + 1 < articles.count + } + + var prevArticleIndexPath: IndexPath? { + guard let indexPath = currentArticleIndexPath else { + return nil + } + return IndexPath(row: indexPath.row - 1, section: indexPath.section) + } + + var nextArticleIndexPath: IndexPath? { + guard let indexPath = currentArticleIndexPath else { + return nil + } + return IndexPath(row: indexPath.row + 1, section: indexPath.section) + } + + var currentArticle: Article? { + if let indexPath = currentArticleIndexPath { + return articles[indexPath.row] + } + return nil + } + + var currentArticleIndexPath: IndexPath? { + didSet { + if currentArticleIndexPath != oldValue { + NotificationCenter.default.post(name: .ArticleSelectionChange, object: self, userInfo: nil) + } + } + } + var articles = ArticleArray() { didSet { if articles == oldValue { @@ -80,8 +128,6 @@ class NavigationModelController { } } - private var articleRowMap = [String: Int]() // articleID: rowIndex - init() { for section in treeController.rootNode.childNodes { @@ -165,6 +211,9 @@ class NavigationModelController { } func nodeFor(_ indexPath: IndexPath) -> Node? { + guard indexPath.section < shadowTable.count || indexPath.row < shadowTable[indexPath.section].count else { + return nil + } return shadowTable[indexPath.section][indexPath.row] } @@ -303,7 +352,7 @@ class NavigationModelController { } -private extension NavigationModelController { +private extension NavigationStateController { // MARK: Fetching Articles @@ -358,7 +407,7 @@ private extension NavigationModelController { } func queueFetchAndMergeArticles() { - NavigationModelController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) + NavigationStateController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) } @objc func fetchAndMergeArticles() { diff --git a/iOS/Timeline/MasterTimelineViewController.swift b/iOS/Timeline/MasterTimelineViewController.swift index d86ab9a5d..50449f9ce 100644 --- a/iOS/Timeline/MasterTimelineViewController.swift +++ b/iOS/Timeline/MasterTimelineViewController.swift @@ -17,10 +17,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner private var rowHeightWithoutFeedName: CGFloat = 0.0 private var currentRowHeight: CGFloat { - return nmc.showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName + return navState?.showFeedNames ?? false ? rowHeightWithFeedName : rowHeightWithoutFeedName } - var nmc: NavigationModelController! + weak var navState: NavigationStateController? var undoableCommands = [UndoableCommand]() var detailViewController: DetailViewController? { @@ -47,10 +47,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: nmc) - NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: nmc) - NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: nmc) - NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: nmc) + NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: navState) + NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: navState) + NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: navState) + NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: navState) + NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionChange(_:)), name: .ArticleSelectionChange, object: navState) refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) @@ -69,14 +70,11 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "showDetail" { - if let indexPath = tableView.indexPathForSelectedRow { - let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController - let article = nmc.articles[indexPath.row] - controller.article = article - controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem - controller.navigationItem.leftItemsSupplementBackButton = true - splitViewController?.toggleMasterView() - } + let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController + controller.navState = navState + controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem + controller.navigationItem.leftItemsSupplementBackButton = true + splitViewController?.toggleMasterView() } } @@ -95,7 +93,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner let markTitle = NSLocalizedString("Mark All Read", comment: "Mark All Read") let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in - guard let articles = self?.nmc.articles, + guard let articles = self?.navState?.articles, let undoManager = self?.undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { return @@ -116,12 +114,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return nmc.articles.count + return navState?.articles.count ?? 0 } override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let article = nmc.articles[indexPath.row] + guard let article = navState?.articles[indexPath.row] else { + return nil + } // Set up the star action let starTitle = article.status.starred ? @@ -165,7 +165,10 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell - let article = nmc.articles[indexPath.row] + + guard let article = navState?.articles[indexPath.row] else { + return cell + } configureTimelineCell(cell, article: article) @@ -173,10 +176,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let article = nmc.articles[indexPath.row] - if !article.status.read { - markArticles(Set([article]), statusKey: .read, flag: true) - } + navState?.currentArticleIndexPath = indexPath } // MARK: Notifications @@ -206,7 +206,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner performBlockAndRestoreSelection { tableView.indexPathsForVisibleRows?.forEach { indexPath in - guard let article = nmc.articles.articleAtRow(indexPath.row) else { + guard let article = navState?.articles.articleAtRow(indexPath.row) else { return } @@ -222,14 +222,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner @objc func avatarDidBecomeAvailable(_ note: Notification) { - guard nmc.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { + guard navState?.showAvatars ?? false, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { return } performBlockAndRestoreSelection { tableView.indexPathsForVisibleRows?.forEach { indexPath in - guard let article = nmc.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else { + guard let article = navState?.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else { return } @@ -245,13 +245,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } @objc func imageDidBecomeAvailable(_ note: Notification) { - if nmc.showAvatars { + if navState?.showAvatars ?? false { queueReloadVisableCells() } } @objc func articlesReinitialized(_ note: Notification) { - if nmc.articles.count > 0 { + if navState?.articles.count ?? 0 > 0 { tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } } @@ -268,6 +268,17 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner tableView.reloadData() } + @objc func articleSelectionChange(_ note: Notification) { + + if let indexPath = navState?.currentArticleIndexPath, let article = navState?.articles[indexPath.row] { + if !article.status.read { + markArticles(Set([article]), statusKey: .read, flag: true) + } + tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) + } + + } + // MARK: Reloading @objc func reloadAllVisibleCells() { @@ -290,8 +301,9 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner if articleIDs.isEmpty { return } - let indexes = nmc.indexesForArticleIDs(articleIDs) - reloadVisibleCells(for: indexes) + if let indexes = navState?.indexesForArticleIDs(articleIDs) { + reloadVisibleCells(for: indexes) + } } private func reloadVisibleCells(for indexes: IndexSet) { @@ -344,13 +356,15 @@ private extension MasterTimelineViewController { } let featuredImage = featuredImageFor(article) - cell.cellData = MasterTimelineCellData(article: article, showFeedName: nmc.showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: nmc.showAvatars, featuredImage: featuredImage) + let showFeedNames = navState?.showFeedNames ?? false + let showAvatars = navState?.showAvatars ?? false + cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage) } func avatarFor(_ article: Article) -> UIImage? { - if !nmc.showAvatars { + if !(navState?.showAvatars ?? false) { return nil }