Rename NavStateController to AppCoordinator because we are redesigning it to be the Coordinator pattern
This commit is contained in:
parent
3fc3903758
commit
fcf2937394
|
@ -10,7 +10,7 @@
|
|||
5110AB7822B7BD6200A94F76 /* AddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5110AB7722B7BD6200A94F76 /* AddView.swift */; };
|
||||
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 /* NavigationStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* NavigationStateController.swift */; };
|
||||
5126EE97226CB48A00C22AFC /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* AppCoordinator.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 */; };
|
||||
|
@ -666,7 +666,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 /* NavigationStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStateController.swift; sourceTree = "<group>"; };
|
||||
5126EE96226CB48A00C22AFC /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.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 /* MasterFeedTableViewSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeader.swift; sourceTree = "<group>"; };
|
||||
|
@ -1709,10 +1709,10 @@
|
|||
84C9FC9F2262A1B300D921D6 /* Main.storyboard */,
|
||||
840D617E2029031C009BC708 /* AppDelegate.swift */,
|
||||
519E743422C663F900A78E47 /* SceneDelegate.swift */,
|
||||
5126EE96226CB48A00C22AFC /* AppCoordinator.swift */,
|
||||
51C45254226507D200C03939 /* AppAssets.swift */,
|
||||
51C45255226507D200C03939 /* AppDefaults.swift */,
|
||||
51E3EB3C229AB08300645299 /* ErrorHandler.swift */,
|
||||
5126EE96226CB48A00C22AFC /* NavigationStateController.swift */,
|
||||
51C4525D226508F600C03939 /* MasterFeed */,
|
||||
51C4526D2265091600C03939 /* MasterTimeline */,
|
||||
51C4527D2265092C00C03939 /* Detail */,
|
||||
|
@ -2346,7 +2346,7 @@
|
|||
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
|
||||
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
|
||||
51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* AppCoordinator.swift in Sources */,
|
||||
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
|
||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||
5183CCEF227125970010922C /* SettingsViewController.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,663 @@
|
|||
//
|
||||
// NavigationModelController.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 4/21/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Account
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
public extension Notification.Name {
|
||||
static let MasterSelectionDidChange = Notification.Name(rawValue: "MasterSelectionDidChange")
|
||||
static let BackingStoresDidRebuild = Notification.Name(rawValue: "BackingStoresDidRebuild")
|
||||
static let ArticlesReinitialized = Notification.Name(rawValue: "ArticlesReinitialized")
|
||||
static let ArticleDataDidChange = Notification.Name(rawValue: "ArticleDataDidChange")
|
||||
static let ArticlesDidChange = Notification.Name(rawValue: "ArticlesDidChange")
|
||||
static let ArticleSelectionDidChange = Notification.Name(rawValue: "ArticleSelectionDidChange")
|
||||
}
|
||||
|
||||
class AppCoordinator {
|
||||
|
||||
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
|
||||
private var animatingChanges = false
|
||||
private var expandedNodes = [Node]()
|
||||
private var shadowTable = [[Node]]()
|
||||
|
||||
private var sortDirection = AppDefaults.timelineSortDirection {
|
||||
didSet {
|
||||
if sortDirection != oldValue {
|
||||
sortDirectionDidChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let treeControllerDelegate = FeedTreeControllerDelegate()
|
||||
lazy var treeController: TreeController = {
|
||||
return TreeController(delegate: treeControllerDelegate)
|
||||
}()
|
||||
|
||||
var rootNode: Node {
|
||||
return treeController.rootNode
|
||||
}
|
||||
|
||||
var numberOfSections: Int {
|
||||
return shadowTable.count
|
||||
}
|
||||
|
||||
var currentMasterIndexPath: IndexPath? {
|
||||
didSet {
|
||||
guard let ip = currentMasterIndexPath, let node = nodeFor(ip) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
if let fetcher = node.representedObject as? ArticleFetcher {
|
||||
timelineFetcher = fetcher
|
||||
}
|
||||
NotificationCenter.default.post(name: .MasterSelectionDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
var timelineName: String? {
|
||||
return (timelineFetcher as? DisplayNameProvider)?.nameForDisplay
|
||||
}
|
||||
|
||||
var timelineFetcher: ArticleFetcher? {
|
||||
didSet {
|
||||
currentArticleIndexPath = nil
|
||||
if timelineFetcher is Feed {
|
||||
showFeedNames = false
|
||||
} else {
|
||||
showFeedNames = true
|
||||
}
|
||||
fetchArticles()
|
||||
NotificationCenter.default.post(name: .ArticlesReinitialized, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var showFeedNames = false
|
||||
var showAvatars = false
|
||||
|
||||
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 firstUnreadArticleIndexPath: IndexPath? {
|
||||
for (row, article) in articles.enumerated() {
|
||||
if !article.status.read {
|
||||
return IndexPath(row: row, section: 0)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentArticle: Article? {
|
||||
if let indexPath = currentArticleIndexPath {
|
||||
return articles[indexPath.row]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentArticleIndexPath: IndexPath? {
|
||||
didSet {
|
||||
if currentArticleIndexPath != oldValue {
|
||||
NotificationCenter.default.post(name: .ArticleSelectionDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var articles = ArticleArray() {
|
||||
didSet {
|
||||
if articles == oldValue {
|
||||
return
|
||||
}
|
||||
if articles.representSameArticlesInSameOrder(as: oldValue) {
|
||||
articleRowMap = [String: Int]()
|
||||
NotificationCenter.default.post(name: .ArticleDataDidChange, object: self, userInfo: nil)
|
||||
return
|
||||
}
|
||||
updateShowAvatars()
|
||||
articleRowMap = [String: Int]()
|
||||
NotificationCenter.default.post(name: .ArticlesDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
var isTimelineUnreadAvailable: Bool {
|
||||
if let unreadProvider = timelineFetcher as? UnreadCountProvider {
|
||||
return unreadProvider.unreadCount > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isAnyUnreadAvailable: Bool {
|
||||
return appDelegate.unreadCount > 0
|
||||
}
|
||||
|
||||
init() {
|
||||
|
||||
for section in treeController.rootNode.childNodes {
|
||||
expandedNodes.append(section)
|
||||
shadowTable.append([Node]())
|
||||
}
|
||||
|
||||
rebuildShadowTable()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func containerChildrenDidChange(_ note: Notification) {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
@objc func batchUpdateDidPerform(_ notification: Notification) {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
@objc func displayNameDidChange(_ note: Notification) {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
@objc func accountStateDidChange(_ note: Notification) {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
@objc func accountsDidChange(_ note: Notification) {
|
||||
rebuildBackingStores()
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
self.sortDirection = AppDefaults.timelineSortDirection
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldFetchAndMergeArticles = timelineFetcherContainsAnyFeed(feeds) || timelineFetcherContainsAnyPseudoFeed()
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func beginUpdates() {
|
||||
animatingChanges = true
|
||||
}
|
||||
|
||||
func endUpdates() {
|
||||
animatingChanges = false
|
||||
}
|
||||
|
||||
func rowsInSection(_ section: Int) -> Int {
|
||||
return shadowTable[section].count
|
||||
}
|
||||
|
||||
func rebuildShadowTable() {
|
||||
|
||||
shadowTable = [[Node]]()
|
||||
|
||||
for i in 0..<treeController.rootNode.numberOfChildNodes {
|
||||
|
||||
var result = [Node]()
|
||||
|
||||
if let nodes = treeController.rootNode.childAtIndex(i)?.childNodes {
|
||||
for node in nodes {
|
||||
result.append(node)
|
||||
if expandedNodes.contains(node) {
|
||||
for child in node.childNodes {
|
||||
result.append(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shadowTable.append(result)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func isExpanded(_ node: Node) -> Bool {
|
||||
return expandedNodes.contains(node)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
func indexPathFor(_ node: Node) -> IndexPath? {
|
||||
for i in 0..<shadowTable.count {
|
||||
if let row = shadowTable[i].firstIndex(of: node) {
|
||||
return IndexPath(row: row, section: i)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expand(section: Int, completion: ([IndexPath]) -> ()) {
|
||||
|
||||
guard let expandNode = treeController.rootNode.childAtIndex(section) else {
|
||||
return
|
||||
}
|
||||
expandedNodes.append(expandNode)
|
||||
|
||||
animatingChanges = true
|
||||
|
||||
var indexPathsToInsert = [IndexPath]()
|
||||
var i = 0
|
||||
|
||||
func addNode(_ node: Node) {
|
||||
indexPathsToInsert.append(IndexPath(row: i, section: section))
|
||||
shadowTable[section].insert(node, at: i)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
for child in expandNode.childNodes {
|
||||
addNode(child)
|
||||
if expandedNodes.contains(child) {
|
||||
for gChild in child.childNodes {
|
||||
addNode(gChild)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completion(indexPathsToInsert)
|
||||
|
||||
animatingChanges = false
|
||||
|
||||
}
|
||||
|
||||
func expand(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) {
|
||||
|
||||
let expandNode = shadowTable[indexPath.section][indexPath.row]
|
||||
expandedNodes.append(expandNode)
|
||||
|
||||
animatingChanges = true
|
||||
|
||||
var indexPathsToInsert = [IndexPath]()
|
||||
for i in 0..<expandNode.childNodes.count {
|
||||
if let child = expandNode.childAtIndex(i) {
|
||||
let nextIndex = indexPath.row + i + 1
|
||||
indexPathsToInsert.append(IndexPath(row: nextIndex, section: indexPath.section))
|
||||
shadowTable[indexPath.section].insert(child, at: nextIndex)
|
||||
}
|
||||
}
|
||||
|
||||
completion(indexPathsToInsert)
|
||||
|
||||
animatingChanges = false
|
||||
|
||||
}
|
||||
|
||||
func collapse(section: Int, completion: ([IndexPath]) -> ()) {
|
||||
|
||||
animatingChanges = true
|
||||
|
||||
guard let collapseNode = treeController.rootNode.childAtIndex(section) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
|
||||
expandedNodes.remove(at: removeNode)
|
||||
}
|
||||
|
||||
var indexPathsToRemove = [IndexPath]()
|
||||
for i in 0..<shadowTable[section].count {
|
||||
indexPathsToRemove.append(IndexPath(row: i, section: section))
|
||||
}
|
||||
shadowTable[section] = [Node]()
|
||||
|
||||
completion(indexPathsToRemove)
|
||||
|
||||
animatingChanges = false
|
||||
|
||||
}
|
||||
|
||||
func collapse(_ indexPath: IndexPath, completion: ([IndexPath]) -> ()) {
|
||||
|
||||
animatingChanges = true
|
||||
|
||||
let collapseNode = shadowTable[indexPath.section][indexPath.row]
|
||||
if let removeNode = expandedNodes.firstIndex(of: collapseNode) {
|
||||
expandedNodes.remove(at: removeNode)
|
||||
}
|
||||
|
||||
var indexPathsToRemove = [IndexPath]()
|
||||
|
||||
for child in collapseNode.childNodes {
|
||||
if let index = shadowTable[indexPath.section].firstIndex(of: child) {
|
||||
indexPathsToRemove.append(IndexPath(row: index, section: indexPath.section))
|
||||
}
|
||||
}
|
||||
|
||||
for child in collapseNode.childNodes {
|
||||
if let index = shadowTable[indexPath.section].firstIndex(of: child) {
|
||||
shadowTable[indexPath.section].remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
completion(indexPathsToRemove)
|
||||
|
||||
animatingChanges = false
|
||||
|
||||
}
|
||||
|
||||
func indexesForArticleIDs(_ articleIDs: Set<String>) -> IndexSet {
|
||||
|
||||
var indexes = IndexSet()
|
||||
|
||||
articleIDs.forEach { (articleID) in
|
||||
guard let oneIndex = row(for: articleID) else {
|
||||
return
|
||||
}
|
||||
if oneIndex != NSNotFound {
|
||||
indexes.insert(oneIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return indexes
|
||||
}
|
||||
|
||||
func selectNextUnread() {
|
||||
|
||||
// This should never happen, but I don't want to risk throwing us
|
||||
// into an infinate loop searching for an unread that isn't there.
|
||||
if appDelegate.unreadCount < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
if selectNextUnreadArticleInTimeline() {
|
||||
return
|
||||
}
|
||||
|
||||
selectNextUnreadFeedFetcher()
|
||||
selectNextUnreadArticleInTimeline()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: UISplitViewControllerDelegate
|
||||
|
||||
extension AppCoordinator: UISplitViewControllerDelegate {
|
||||
|
||||
func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController:UIViewController, onto primaryViewController:UIViewController) -> Bool {
|
||||
if currentArticle == nil {
|
||||
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension AppCoordinator {
|
||||
|
||||
func rebuildBackingStores() {
|
||||
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
||||
treeController.rebuild()
|
||||
rebuildShadowTable()
|
||||
NotificationCenter.default.post(name: .BackingStoresDidRebuild, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func updateShowAvatars() {
|
||||
|
||||
if showFeedNames {
|
||||
self.showAvatars = true
|
||||
return
|
||||
}
|
||||
|
||||
for article in articles {
|
||||
if let authors = article.authors {
|
||||
for author in authors {
|
||||
if author.avatarURL != nil {
|
||||
self.showAvatars = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.showAvatars = false
|
||||
}
|
||||
|
||||
// MARK: Select Next Unread
|
||||
|
||||
@discardableResult
|
||||
func selectNextUnreadArticleInTimeline() -> Bool {
|
||||
|
||||
let startingRow: Int = {
|
||||
if let indexPath = currentArticleIndexPath {
|
||||
return indexPath.row
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}()
|
||||
|
||||
for i in startingRow..<articles.count {
|
||||
let article = articles[i]
|
||||
if !article.status.read {
|
||||
currentArticleIndexPath = IndexPath(row: i, section: 0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func selectNextUnreadFeedFetcher() {
|
||||
|
||||
guard let indexPath = currentMasterIndexPath else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// Increment or wrap around the IndexPath
|
||||
let nextIndexPath: IndexPath = {
|
||||
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
|
||||
if indexPath.section + 1 >= shadowTable.count {
|
||||
return IndexPath(row: 0, section: 0)
|
||||
} else {
|
||||
return IndexPath(row: 0, section: indexPath.section + 1)
|
||||
}
|
||||
} else {
|
||||
return IndexPath(row: indexPath.row + 1, section: indexPath.section)
|
||||
}
|
||||
}()
|
||||
|
||||
if selectNextUnreadFeedFetcher(startingWith: nextIndexPath) {
|
||||
return
|
||||
}
|
||||
selectNextUnreadFeedFetcher(startingWith: IndexPath(row: 0, section: 0))
|
||||
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectNextUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool {
|
||||
|
||||
for i in indexPath.section..<shadowTable.count {
|
||||
|
||||
for j in indexPath.row..<shadowTable[indexPath.section].count {
|
||||
|
||||
let nextIndexPath = IndexPath(row: j, section: i)
|
||||
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {
|
||||
assertionFailure()
|
||||
return true
|
||||
}
|
||||
|
||||
if expandedNodes.contains(node) {
|
||||
continue
|
||||
}
|
||||
|
||||
if unreadCountProvider.unreadCount > 0 {
|
||||
currentMasterIndexPath = nextIndexPath
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// MARK: Fetching Articles
|
||||
|
||||
func fetchArticles() {
|
||||
|
||||
guard let timelineFetcher = timelineFetcher else {
|
||||
articles = ArticleArray()
|
||||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = timelineFetcher.fetchArticles()
|
||||
updateArticles(with: fetchedArticles)
|
||||
|
||||
}
|
||||
|
||||
func emptyTheTimeline() {
|
||||
if !articles.isEmpty {
|
||||
articles = [Article]()
|
||||
}
|
||||
}
|
||||
|
||||
func sortDirectionDidChange() {
|
||||
updateArticles(with: Set(articles))
|
||||
}
|
||||
|
||||
func updateArticles(with unsortedArticles: Set<Article>) {
|
||||
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
|
||||
if articles != sortedArticles {
|
||||
articles = sortedArticles
|
||||
}
|
||||
}
|
||||
|
||||
func row(for articleID: String) -> Int? {
|
||||
updateArticleRowMapIfNeeded()
|
||||
return articleRowMap[articleID]
|
||||
}
|
||||
|
||||
func updateArticleRowMap() {
|
||||
var rowMap = [String: Int]()
|
||||
var index = 0
|
||||
articles.forEach { (article) in
|
||||
rowMap[article.articleID] = index
|
||||
index += 1
|
||||
}
|
||||
articleRowMap = rowMap
|
||||
}
|
||||
|
||||
func updateArticleRowMapIfNeeded() {
|
||||
if articleRowMap.isEmpty {
|
||||
updateArticleRowMap()
|
||||
}
|
||||
}
|
||||
|
||||
func queueFetchAndMergeArticles() {
|
||||
AppCoordinator.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
||||
}
|
||||
|
||||
@objc func fetchAndMergeArticles() {
|
||||
|
||||
guard let timelineFetcher = timelineFetcher else {
|
||||
return
|
||||
}
|
||||
|
||||
var unsortedArticles = timelineFetcher.fetchArticles()
|
||||
|
||||
// Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles.
|
||||
let unsortedArticleIDs = unsortedArticles.articleIDs()
|
||||
for article in articles {
|
||||
if !unsortedArticleIDs.contains(article.articleID) {
|
||||
unsortedArticles.insert(article)
|
||||
}
|
||||
}
|
||||
|
||||
updateArticles(with: unsortedArticles)
|
||||
|
||||
}
|
||||
|
||||
func timelineFetcherContainsAnyPseudoFeed() -> Bool {
|
||||
if timelineFetcher is PseudoFeed {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func timelineFetcherContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
if let feed = timelineFetcher as? Feed {
|
||||
for oneFeed in feeds {
|
||||
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if let folder = timelineFetcher as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -23,7 +23,7 @@ class DetailViewController: UIViewController {
|
|||
@IBOutlet weak var browserBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var webView: WKWebView!
|
||||
|
||||
weak var navState: NavigationStateController?
|
||||
weak var coordinator: AppCoordinator?
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
|
@ -36,7 +36,7 @@ class DetailViewController: UIViewController {
|
|||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionDidChange(_:)), name: .ArticleSelectionDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionDidChange(_:)), name: .ArticleSelectionDidChange, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
@ -47,14 +47,14 @@ class DetailViewController: UIViewController {
|
|||
}
|
||||
|
||||
func markAsRead() {
|
||||
if let article = navState?.currentArticle {
|
||||
if let article = coordinator?.currentArticle {
|
||||
markArticles(Set([article]), statusKey: .read, flag: true)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUI() {
|
||||
|
||||
guard let article = navState?.currentArticle else {
|
||||
guard let article = coordinator?.currentArticle else {
|
||||
nextUnreadBarButtonItem.isEnabled = false
|
||||
prevArticleBarButtonItem.isEnabled = false
|
||||
nextArticleBarButtonItem.isEnabled = false
|
||||
|
@ -65,9 +65,9 @@ class DetailViewController: UIViewController {
|
|||
return
|
||||
}
|
||||
|
||||
nextUnreadBarButtonItem.isEnabled = navState?.isAnyUnreadAvailable ?? false
|
||||
prevArticleBarButtonItem.isEnabled = navState?.isPrevArticleAvailable ?? false
|
||||
nextArticleBarButtonItem.isEnabled = navState?.isNextArticleAvailable ?? false
|
||||
nextUnreadBarButtonItem.isEnabled = coordinator?.isAnyUnreadAvailable ?? false
|
||||
prevArticleBarButtonItem.isEnabled = coordinator?.isPrevArticleAvailable ?? false
|
||||
nextArticleBarButtonItem.isEnabled = coordinator?.isNextArticleAvailable ?? false
|
||||
|
||||
readBarButtonItem.isEnabled = true
|
||||
starBarButtonItem.isEnabled = true
|
||||
|
@ -80,7 +80,7 @@ class DetailViewController: UIViewController {
|
|||
let starImage = article.status.starred ? AppAssets.starClosedImage : AppAssets.starOpenImage
|
||||
starBarButtonItem.image = starImage
|
||||
|
||||
if let timelineName = navState?.timelineName {
|
||||
if let timelineName = coordinator?.timelineName {
|
||||
if navigationController?.navigationItem.backBarButtonItem?.title != timelineName {
|
||||
let backItem = UIBarButtonItem(title: timelineName, style: .plain, target: nil, action: nil)
|
||||
navigationController?.navigationItem.backBarButtonItem = backItem
|
||||
|
@ -91,7 +91,7 @@ class DetailViewController: UIViewController {
|
|||
|
||||
func reloadHTML() {
|
||||
|
||||
guard let article = navState?.currentArticle, let webView = webView else {
|
||||
guard let article = coordinator?.currentArticle, let webView = webView else {
|
||||
return
|
||||
}
|
||||
let style = ArticleStylesManager.shared.currentStyle
|
||||
|
@ -110,7 +110,7 @@ class DetailViewController: UIViewController {
|
|||
guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set<Article> else {
|
||||
return
|
||||
}
|
||||
if articles.count == 1 && articles.first?.articleID == navState?.currentArticle?.articleID {
|
||||
if articles.count == 1 && articles.first?.articleID == coordinator?.currentArticle?.articleID {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
@ -132,41 +132,41 @@ class DetailViewController: UIViewController {
|
|||
// MARK: Actions
|
||||
|
||||
@IBAction func nextUnread(_ sender: Any) {
|
||||
navState?.selectNextUnread()
|
||||
coordinator?.selectNextUnread()
|
||||
}
|
||||
|
||||
@IBAction func prevArticle(_ sender: Any) {
|
||||
navState?.currentArticleIndexPath = navState?.prevArticleIndexPath
|
||||
coordinator?.currentArticleIndexPath = coordinator?.prevArticleIndexPath
|
||||
}
|
||||
|
||||
@IBAction func nextArticle(_ sender: Any) {
|
||||
navState?.currentArticleIndexPath = navState?.nextArticleIndexPath
|
||||
coordinator?.currentArticleIndexPath = coordinator?.nextArticleIndexPath
|
||||
}
|
||||
|
||||
@IBAction func toggleRead(_ sender: Any) {
|
||||
if let article = navState?.currentArticle {
|
||||
if let article = coordinator?.currentArticle {
|
||||
markArticles(Set([article]), statusKey: .read, flag: !article.status.read)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleStar(_ sender: Any) {
|
||||
if let article = navState?.currentArticle {
|
||||
if let article = coordinator?.currentArticle {
|
||||
markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func openBrowser(_ sender: Any) {
|
||||
guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
guard let preferredLink = coordinator?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
}
|
||||
UIApplication.shared.open(url, options: [:])
|
||||
}
|
||||
|
||||
@IBAction func showActivityDialog(_ sender: Any) {
|
||||
guard let preferredLink = navState?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
guard let preferredLink = coordinator?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
}
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: navState?.currentArticle?.title)
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: coordinator?.currentArticle?.title)
|
||||
let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil)
|
||||
activityViewController.popoverPresentationController?.barButtonItem = self.actionBarButtonItem
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
let navState = NavigationStateController()
|
||||
weak var coordinator: AppCoordinator!
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(backingStoresDidRebuild(_:)), name: .BackingStoresDidRebuild, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(masterSelectionDidChange(_:)), name: .MasterSelectionDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(backingStoresDidRebuild(_:)), name: .BackingStoresDidRebuild, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(masterSelectionDidChange(_:)), name: .MasterSelectionDidChange, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
|
@ -82,8 +82,8 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
}
|
||||
|
||||
if let account = representedObject as? Account {
|
||||
if let node = navState.rootNode.childNodeRepresentingObject(account) {
|
||||
let sectionIndex = navState.rootNode.indexOfChild(node)!
|
||||
if let node = coordinator.rootNode.childNodeRepresentingObject(account) {
|
||||
let sectionIndex = coordinator.rootNode.indexOfChild(node)!
|
||||
if let headerView = tableView.headerView(forSection: sectionIndex) as? MasterFeedTableViewSectionHeader {
|
||||
headerView.unreadCount = account.unreadCount
|
||||
}
|
||||
|
@ -91,8 +91,8 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
return
|
||||
}
|
||||
|
||||
guard let node = navState.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject),
|
||||
let indexPath = navState.indexPathFor(node) else {
|
||||
guard let node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject),
|
||||
let indexPath = coordinator.indexPathFor(node) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -119,27 +119,27 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
@objc func userDidAddFeed(_ notification: Notification) {
|
||||
|
||||
guard let feed = notification.userInfo?[UserInfoKey.feed],
|
||||
let node = navState.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
|
||||
let node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) else {
|
||||
return
|
||||
}
|
||||
|
||||
if let indexPath = navState.indexPathFor(node) {
|
||||
if let indexPath = coordinator.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 = navState.indexPathFor(parent) else {
|
||||
guard let parent = node.parent, let indexPath = coordinator.indexPathFor(parent) else {
|
||||
return
|
||||
}
|
||||
|
||||
navState.expand(indexPath) { [weak self] indexPaths in
|
||||
coordinator.expand(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
}
|
||||
|
||||
if let indexPath = navState.indexPathFor(node) {
|
||||
if let indexPath = coordinator.indexPathFor(node) {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
}
|
||||
|
||||
|
@ -154,7 +154,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
}
|
||||
|
||||
@objc func masterSelectionDidChange(_ note: Notification) {
|
||||
if let indexPath = navState.currentMasterIndexPath {
|
||||
if let indexPath = coordinator.currentMasterIndexPath {
|
||||
if tableView.indexPathForSelectedRow != indexPath {
|
||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
|
||||
}
|
||||
|
@ -168,16 +168,16 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
// MARK: Table View
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return navState.numberOfSections
|
||||
return coordinator.numberOfSections
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return navState.rowsInSection(section)
|
||||
return coordinator.rowsInSection(section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
|
||||
guard let nameProvider = navState.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
return 44
|
||||
}
|
||||
|
||||
|
@ -191,14 +191,14 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
|
||||
guard let nameProvider = navState.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! MasterFeedTableViewSectionHeader
|
||||
headerView.name = nameProvider.nameForDisplay
|
||||
|
||||
guard let sectionNode = navState.rootNode.childAtIndex(section) else {
|
||||
guard let sectionNode = coordinator.rootNode.childAtIndex(section) else {
|
||||
return headerView
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
}
|
||||
|
||||
headerView.tag = section
|
||||
headerView.disclosureExpanded = navState.isExpanded(sectionNode)
|
||||
headerView.disclosureExpanded = coordinator.isExpanded(sectionNode)
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:)))
|
||||
headerView.addGestureRecognizer(tap)
|
||||
|
@ -230,7 +230,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell
|
||||
|
||||
guard let node = navState.nodeFor(indexPath) else {
|
||||
guard let node = coordinator.nodeFor(indexPath) else {
|
||||
return cell
|
||||
}
|
||||
|
||||
|
@ -240,7 +240,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let node = navState.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
|
||||
guard let node = coordinator.nodeFor(indexPath), !(node.representedObject is PseudoFeed) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
@ -273,14 +273,14 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
||||
timeline.navState = navState
|
||||
navState.currentMasterIndexPath = indexPath
|
||||
timeline.coordinator = coordinator
|
||||
coordinator.currentMasterIndexPath = indexPath
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let node = navState.nodeFor(indexPath) else {
|
||||
guard let node = coordinator.nodeFor(indexPath) else {
|
||||
return false
|
||||
}
|
||||
return node.representedObject is Feed
|
||||
|
@ -296,13 +296,13 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
return proposedDestinationIndexPath
|
||||
}()
|
||||
|
||||
guard let draggedNode = navState.nodeFor(sourceIndexPath), let destNode = navState.nodeFor(destIndexPath), let parentNode = destNode.parent else {
|
||||
guard let draggedNode = coordinator.nodeFor(sourceIndexPath), let destNode = coordinator.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 || !navState.isExpanded(destNode)) {
|
||||
if destNode.representedObject is Folder && (destNode.numberOfChildNodes == 0 || !coordinator.isExpanded(destNode)) {
|
||||
let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0
|
||||
return IndexPath(row: destIndexPath.row + movementAdjustment, section: destIndexPath.section)
|
||||
}
|
||||
|
@ -323,7 +323,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
if parentNode.representedObject is Account {
|
||||
return IndexPath(row: 0, section: destIndexPath.section)
|
||||
} else {
|
||||
return navState.indexPathFor(parentNode)!
|
||||
return coordinator.indexPathFor(parentNode)!
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -333,10 +333,10 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0
|
||||
let adjustedIndex = index - movementAdjustment
|
||||
if adjustedIndex >= sortedNodes.count {
|
||||
let lastSortedIndexPath = navState.indexPathFor(sortedNodes[sortedNodes.count - 1])!
|
||||
let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1])!
|
||||
return IndexPath(row: lastSortedIndexPath.row + 1, section: lastSortedIndexPath.section)
|
||||
} else {
|
||||
return navState.indexPathFor(sortedNodes[adjustedIndex])!
|
||||
return coordinator.indexPathFor(sortedNodes[adjustedIndex])!
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -345,18 +345,18 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
|
||||
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||
|
||||
guard let sourceNode = navState.nodeFor(sourceIndexPath), let feed = sourceNode.representedObject as? Feed else {
|
||||
guard let sourceNode = coordinator.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 navState.rootNode.childAtIndex(destinationIndexPath.section)!
|
||||
return coordinator.rootNode.childAtIndex(destinationIndexPath.section)!
|
||||
} else {
|
||||
let movementAdjustment = sourceIndexPath > destinationIndexPath ? 1 : 0
|
||||
let adjustedDestIndexPath = IndexPath(row: destinationIndexPath.row - movementAdjustment, section: destinationIndexPath.section)
|
||||
return navState.nodeFor(adjustedDestIndexPath)!
|
||||
return coordinator.nodeFor(adjustedDestIndexPath)!
|
||||
}
|
||||
}()
|
||||
|
||||
|
@ -453,22 +453,22 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
@objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) {
|
||||
|
||||
guard let sectionIndex = sender.view?.tag,
|
||||
let sectionNode = navState.rootNode.childAtIndex(sectionIndex),
|
||||
let sectionNode = coordinator.rootNode.childAtIndex(sectionIndex),
|
||||
let headerView = sender.view as? MasterFeedTableViewSectionHeader
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if navState.isExpanded(sectionNode) {
|
||||
if coordinator.isExpanded(sectionNode) {
|
||||
headerView.disclosureExpanded = false
|
||||
navState.collapse(section: sectionIndex) { [weak self] indexPaths in
|
||||
coordinator.collapse(section: sectionIndex) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
}
|
||||
} else {
|
||||
headerView.disclosureExpanded = true
|
||||
navState.expand(section: sectionIndex) { [weak self] indexPaths in
|
||||
coordinator.expand(section: sectionIndex) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
@ -487,7 +487,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
} else {
|
||||
cell.indentationLevel = 0
|
||||
}
|
||||
cell.disclosureExpanded = navState.isExpanded(node)
|
||||
cell.disclosureExpanded = coordinator.isExpanded(node)
|
||||
cell.allowDisclosureSelection = node.canHaveChildNodes
|
||||
|
||||
cell.name = nameFor(node)
|
||||
|
@ -525,14 +525,14 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
func delete(indexPath: IndexPath) {
|
||||
|
||||
guard let undoManager = undoManager,
|
||||
let deleteNode = navState.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: navState.treeController, undoManager: undoManager, errorHandler: ErrorHandler.present(self))
|
||||
let deleteNode = coordinator.nodeFor(indexPath),
|
||||
let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], treeController: coordinator.treeController, undoManager: undoManager, errorHandler: ErrorHandler.present(self))
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
var deleteIndexPaths = [indexPath]
|
||||
if navState.isExpanded(deleteNode) {
|
||||
if coordinator.isExpanded(deleteNode) {
|
||||
for i in 0..<deleteNode.numberOfChildNodes {
|
||||
deleteIndexPaths.append(IndexPath(row: indexPath.row + 1 + i, section: indexPath.section))
|
||||
}
|
||||
|
@ -540,19 +540,19 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
|
||||
pushUndoableCommand(deleteCommand)
|
||||
|
||||
navState.beginUpdates()
|
||||
coordinator.beginUpdates()
|
||||
deleteCommand.perform {
|
||||
self.navState.treeController.rebuild()
|
||||
self.navState.rebuildShadowTable()
|
||||
self.coordinator.treeController.rebuild()
|
||||
self.coordinator.rebuildShadowTable()
|
||||
self.tableView.deleteRows(at: deleteIndexPaths, with: .automatic)
|
||||
self.navState.endUpdates()
|
||||
self.coordinator.endUpdates()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
|
||||
let name = (navState.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
|
||||
let name = (coordinator.nodeFor(indexPath)?.representedObject as? DisplayNameProvider)?.nameForDisplay ?? ""
|
||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Feed finder")
|
||||
let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String
|
||||
|
||||
|
@ -564,7 +564,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
|
||||
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
|
||||
|
||||
guard let node = self?.navState.nodeFor(indexPath),
|
||||
guard let node = self?.coordinator.nodeFor(indexPath),
|
||||
let name = alertController.textFields?[0].text,
|
||||
!name.isEmpty else {
|
||||
return
|
||||
|
@ -630,7 +630,7 @@ private extension MasterFeedViewController {
|
|||
}
|
||||
|
||||
func updateUI() {
|
||||
markAllAsReadButton.isEnabled = navState.isAnyUnreadAvailable
|
||||
markAllAsReadButton.isEnabled = coordinator.isAnyUnreadAvailable
|
||||
addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
|
@ -649,7 +649,7 @@ private extension MasterFeedViewController {
|
|||
|
||||
func applyToAvailableCells(_ callback: (MasterFeedTableViewCell, Node) -> Void) {
|
||||
tableView.visibleCells.forEach { cell in
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = navState.nodeFor(indexPath) else {
|
||||
guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else {
|
||||
return
|
||||
}
|
||||
callback(cell as! MasterFeedTableViewCell, node)
|
||||
|
@ -673,7 +673,7 @@ private extension MasterFeedViewController {
|
|||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
navState.expand(indexPath) { [weak self] indexPaths in
|
||||
coordinator.expand(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
@ -684,7 +684,7 @@ private extension MasterFeedViewController {
|
|||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
return
|
||||
}
|
||||
navState.collapse(indexPath) { [weak self] indexPaths in
|
||||
coordinator.collapse(indexPath) { [weak self] indexPaths in
|
||||
self?.tableView.beginUpdates()
|
||||
self?.tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
self?.tableView.endUpdates()
|
||||
|
|
|
@ -18,7 +18,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
||||
@IBOutlet weak var firstUnreadButton: UIBarButtonItem!
|
||||
|
||||
weak var navState: NavigationStateController?
|
||||
weak var coordinator: AppCoordinator!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
|
@ -37,10 +37,10 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, 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(articleSelectionDidChange(_:)), name: .ArticleSelectionDidChange, object: navState)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleSelectionDidChange(_:)), name: .ArticleSelectionDidChange, object: coordinator)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
|
@ -66,7 +66,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
if segue.identifier == "showDetail" {
|
||||
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
|
||||
controller.navState = navState
|
||||
controller.coordinator = coordinator
|
||||
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
|
||||
controller.navigationItem.leftItemsSupplementBackButton = true
|
||||
splitViewController?.toggleMasterView()
|
||||
|
@ -101,7 +101,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
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?.navState?.articles,
|
||||
guard let articles = self?.coordinator.articles,
|
||||
let undoManager = self?.undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
|
@ -119,7 +119,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
}
|
||||
|
||||
@IBAction func firstUnread(_ sender: Any) {
|
||||
if let indexPath = navState?.firstUnreadArticleIndexPath {
|
||||
if let indexPath = coordinator.firstUnreadArticleIndexPath {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
|
||||
}
|
||||
}
|
||||
|
@ -131,14 +131,12 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return navState?.articles.count ?? 0
|
||||
return coordinator.articles.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
guard let article = navState?.articles[indexPath.row] else {
|
||||
return nil
|
||||
}
|
||||
let article = coordinator.articles[indexPath.row]
|
||||
|
||||
// Set up the read action
|
||||
let readTitle = article.status.read ?
|
||||
|
@ -180,20 +178,14 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTimelineTableViewCell
|
||||
|
||||
guard let article = navState?.articles[indexPath.row] else {
|
||||
return cell
|
||||
}
|
||||
|
||||
let article = coordinator.articles[indexPath.row]
|
||||
configureTimelineCell(cell, article: article)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
navState?.currentArticleIndexPath = indexPath
|
||||
coordinator.currentArticleIndexPath = indexPath
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
@ -219,7 +211,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = navState?.articles.articleAtRow(indexPath.row) else {
|
||||
guard let article = coordinator.articles.articleAtRow(indexPath.row) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -235,14 +227,14 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
|
||||
guard navState?.showAvatars ?? false, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
guard coordinator.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = navState?.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
guard let article = coordinator.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -258,7 +250,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
}
|
||||
|
||||
@objc func imageDidBecomeAvailable(_ note: Notification) {
|
||||
if navState?.showAvatars ?? false {
|
||||
if coordinator.showAvatars {
|
||||
queueReloadVisableCells()
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +279,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
|
||||
@objc func articleSelectionDidChange(_ note: Notification) {
|
||||
|
||||
if let indexPath = navState?.currentArticleIndexPath {
|
||||
if let indexPath = coordinator.currentArticleIndexPath {
|
||||
if tableView.indexPathForSelectedRow != indexPath {
|
||||
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
|
||||
}
|
||||
|
@ -323,10 +315,9 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
|||
if articleIDs.isEmpty {
|
||||
return
|
||||
}
|
||||
if let indexes = navState?.indexesForArticleIDs(articleIDs) {
|
||||
let indexes = coordinator.indexesForArticleIDs(articleIDs)
|
||||
reloadVisibleCells(for: indexes)
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadVisibleCells(for indexes: IndexSet) {
|
||||
performBlockAndRestoreSelection {
|
||||
|
@ -373,10 +364,10 @@ private extension MasterTimelineViewController {
|
|||
|
||||
func resetUI() {
|
||||
|
||||
title = navState?.timelineName
|
||||
navigationController?.title = navState?.timelineName
|
||||
title = coordinator.timelineName
|
||||
navigationController?.title = coordinator.timelineName
|
||||
|
||||
if navState?.articles.count ?? 0 > 0 {
|
||||
if coordinator.articles.count > 0 {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
||||
}
|
||||
|
||||
|
@ -385,8 +376,8 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
|
||||
func updateUI() {
|
||||
markAllAsReadButton.isEnabled = navState?.isTimelineUnreadAvailable ?? false
|
||||
firstUnreadButton.isEnabled = navState?.isTimelineUnreadAvailable ?? false
|
||||
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
}
|
||||
|
||||
func configureTimelineCell(_ cell: MasterTimelineTableViewCell, article: Article) {
|
||||
|
@ -394,15 +385,15 @@ private extension MasterTimelineViewController {
|
|||
let avatar = avatarFor(article)
|
||||
let featuredImage = featuredImageFor(article)
|
||||
|
||||
let showFeedNames = navState?.showFeedNames ?? false
|
||||
let showAvatar = navState?.showAvatars ?? false && avatar != nil
|
||||
let showFeedNames = coordinator.showFeedNames
|
||||
let showAvatar = coordinator.showAvatars && avatar != nil
|
||||
cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatar, featuredImage: featuredImage, numberOfLines: numberOfTextLines)
|
||||
|
||||
}
|
||||
|
||||
func avatarFor(_ article: Article) -> UIImage? {
|
||||
|
||||
if !(navState?.showAvatars ?? false) {
|
||||
if !coordinator.showAvatars {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDelegate {
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var coordinator = AppCoordinator()
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
@ -19,9 +21,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
|
|||
window!.tintColor = AppAssets.netNewsWireBlueColor
|
||||
|
||||
let splitViewController = UIStoryboard.main.instantiateInitialViewController() as! UISplitViewController
|
||||
splitViewController.delegate = self
|
||||
splitViewController.delegate = coordinator
|
||||
window!.rootViewController = splitViewController
|
||||
|
||||
let masterNavigationController = splitViewController.viewControllers[0] as! UINavigationController
|
||||
let masterFeedViewController = masterNavigationController.topViewController as! MasterFeedViewController
|
||||
masterFeedViewController.coordinator = coordinator
|
||||
|
||||
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
|
||||
navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem
|
||||
|
||||
|
@ -67,16 +73,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
|
|||
// return false
|
||||
// }
|
||||
|
||||
// MARK: UISplitViewControllerDelegate
|
||||
|
||||
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.navState?.currentArticle == nil {
|
||||
// Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue