Implement detail next and prev article buttons.

This commit is contained in:
Maurice Parker 2019-04-21 17:42:26 -05:00
parent 73500e0244
commit e81defb934
8 changed files with 216 additions and 110 deletions

View File

@ -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)
}

View File

@ -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 = "<group>"; };
51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = "<group>"; };
51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = "<group>"; };
5126EE96226CB48A00C22AFC /* NavigationModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModelController.swift; sourceTree = "<group>"; };
5126EE96226CB48A00C22AFC /* NavigationStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStateController.swift; sourceTree = "<group>"; };
5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailKeyboardDelegate.swift; sourceTree = "<group>"; };
5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DetailKeyboardShortcuts.plist; sourceTree = "<group>"; };
512E08F722688F7C00BDCFDD /* MasterTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTableViewSectionHeader.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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
}

View File

@ -57,11 +57,23 @@
<viewLayoutGuide key="safeArea" id="VUw-jc-0yf"/>
</view>
<toolbarItems>
<barButtonItem enabled="NO" image="nextUnreadImage" id="2w5-e9-C2V"/>
<barButtonItem enabled="NO" image="nextUnreadImage" id="2w5-e9-C2V">
<connections>
<action selector="nextUnread:" destination="JEX-9P-axG" id="USD-hC-C6z"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="vAq-iW-Yyo"/>
<barButtonItem image="prevArticleImage" style="plain" id="v4j-fq-23N"/>
<barButtonItem image="prevArticleImage" style="plain" id="v4j-fq-23N">
<connections>
<action selector="prevArticle:" destination="JEX-9P-axG" id="cMZ-tk-I4W"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="G92-nw-kXs"/>
<barButtonItem image="nextArticleImage" id="2qz-M5-Yhk"/>
<barButtonItem image="nextArticleImage" id="2qz-M5-Yhk">
<connections>
<action selector="nextArticle:" destination="JEX-9P-axG" id="P77-KM-j8D"/>
</connections>
</barButtonItem>
<barButtonItem style="plain" systemItem="flexibleSpace" id="Myj-ux-Zc5"/>
<barButtonItem image="circleOpenImage" id="hy0-LS-MzE">
<connections>
@ -92,6 +104,9 @@
<connections>
<outlet property="actionBarButtonItem" destination="9Ut-5B-JKP" id="9bO-kz-cTz"/>
<outlet property="browserBarButtonItem" destination="DMh-3X-ebd" id="PkT-Tn-8kG"/>
<outlet property="nextArticleBarButtonItem" destination="2qz-M5-Yhk" id="IQd-jx-qEr"/>
<outlet property="nextUnreadBarButtonItem" destination="2w5-e9-C2V" id="xJr-5y-p1N"/>
<outlet property="prevArticleBarButtonItem" destination="v4j-fq-23N" id="Gny-Oh-cQa"/>
<outlet property="readBarButtonItem" destination="hy0-LS-MzE" id="BzM-x9-tuj"/>
<outlet property="starBarButtonItem" destination="wU4-eH-wC9" id="Z8Q-Lt-dKk"/>
<outlet property="webView" destination="t8d-md-Yhc" id="Iqg-bg-wds"/>

View File

@ -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<Article> 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)
}

View File

@ -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()

View File

@ -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() {

View File

@ -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
}