implement model controller pattern
This commit is contained in:
parent
17d83928a9
commit
c5a891234d
|
@ -87,7 +87,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
}
|
||||
|
||||
private var showAvatars = false
|
||||
private var rowHeightWithFeedName: CGFloat = 0.0
|
||||
private var rowHeightWithoutFeedName: CGFloat = 0.0
|
||||
|
||||
|
|
|
@ -9,6 +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 /* AppModelController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* AppModelController.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 */; };
|
||||
|
@ -602,6 +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 /* AppModelController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModelController.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>"; };
|
||||
|
@ -1512,6 +1514,7 @@
|
|||
840D617E2029031C009BC708 /* AppDelegate.swift */,
|
||||
51C45254226507D200C03939 /* AppAssets.swift */,
|
||||
51C45255226507D200C03939 /* AppDefaults.swift */,
|
||||
5126EE96226CB48A00C22AFC /* AppModelController.swift */,
|
||||
51C4525D226508F600C03939 /* Master */,
|
||||
51C4526D2265091600C03939 /* Timeline */,
|
||||
51C4527D2265092C00C03939 /* Detail */,
|
||||
|
@ -1784,12 +1787,12 @@
|
|||
ORGANIZATIONNAME = "Ranchero Software";
|
||||
TargetAttributes = {
|
||||
6581C73220CED60000F4AD34 = {
|
||||
DevelopmentTeam = M8L2WTLA8W;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
840D617B2029031C009BC708 = {
|
||||
CreatedOnToolsVersion = 9.3;
|
||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
840D61902029031D009BC708 = {
|
||||
|
@ -1800,7 +1803,7 @@
|
|||
};
|
||||
849C645F1ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = M8L2WTLA8W;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Manual;
|
||||
SystemCapabilities = {
|
||||
com.apple.HardenedRuntime = {
|
||||
|
@ -1810,7 +1813,7 @@
|
|||
};
|
||||
849C64701ED37A5D003D8FC0 = {
|
||||
CreatedOnToolsVersion = 8.2.1;
|
||||
DevelopmentTeam = 9C84TZ7Q6Z;
|
||||
DevelopmentTeam = SHJK2V3AJG;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 849C645F1ED37A5D003D8FC0;
|
||||
};
|
||||
|
@ -2146,6 +2149,7 @@
|
|||
51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */,
|
||||
51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */,
|
||||
51C4526B226508F600C03939 /* MasterViewController.swift in Sources */,
|
||||
5126EE97226CB48A00C22AFC /* AppModelController.swift in Sources */,
|
||||
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
|
||||
51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */,
|
||||
51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
public extension Notification.Name {
|
||||
static let ShowFeedNamesDidChange = Notification.Name(rawValue: "ShowFeedNamesDidChange")
|
||||
static let ArticlesReinitialized = Notification.Name(rawValue: "ArticlesReinitialized")
|
||||
static let ArticleDataDidChange = Notification.Name(rawValue: "ArticleDataDidChange")
|
||||
static let ArticlesDidChange = Notification.Name(rawValue: "ArticlesDidChange")
|
||||
}
|
||||
|
||||
class AppModelController {
|
||||
|
||||
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
}
|
||||
|
||||
private var sortDirection = AppDefaults.timelineSortDirection {
|
||||
didSet {
|
||||
if sortDirection != oldValue {
|
||||
sortDirectionDidChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showFeedNames = false {
|
||||
didSet {
|
||||
NotificationCenter.default.post(name: .ShowFeedNamesDidChange, object: self, userInfo: nil)
|
||||
}
|
||||
}
|
||||
var showAvatars = false
|
||||
|
||||
var timelineFetcher: ArticleFetcher? {
|
||||
didSet {
|
||||
if timelineFetcher is Feed {
|
||||
showFeedNames = false
|
||||
} else {
|
||||
showFeedNames = true
|
||||
}
|
||||
fetchArticles()
|
||||
NotificationCenter.default.post(name: .ArticlesReinitialized, 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)
|
||||
}
|
||||
}
|
||||
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@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 = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension AppModelController {
|
||||
|
||||
// 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() {
|
||||
AppModelController.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 representedObjectsContainsAnyPseudoFeed() -> Bool {
|
||||
if timelineFetcher is PseudoFeed {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyFeed(_ 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
|
||||
|
||||
}
|
||||
|
||||
// MARK: Misc
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -20,6 +20,7 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
var expandedNodes = [Node]()
|
||||
var shadowTable = [[Node]]()
|
||||
|
||||
let appModelController = AppModelController()
|
||||
let treeControllerDelegate = FeedTreeControllerDelegate()
|
||||
lazy var treeController: TreeController = {
|
||||
return TreeController(delegate: treeControllerDelegate)
|
||||
|
@ -267,21 +268,16 @@ class MasterViewController: UITableViewController, UndoableCommandRunner {
|
|||
|
||||
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
|
||||
|
||||
if let pseudoFeed = node.representedObject as? PseudoFeed {
|
||||
timeline.title = pseudoFeed.nameForDisplay
|
||||
timeline.representedObjects = [pseudoFeed]
|
||||
if let fetcher = node.representedObject as? ArticleFetcher {
|
||||
appModelController.timelineFetcher = fetcher
|
||||
}
|
||||
|
||||
if let folder = node.representedObject as? Folder {
|
||||
timeline.title = folder.nameForDisplay
|
||||
timeline.representedObjects = [folder]
|
||||
}
|
||||
|
||||
if let feed = node.representedObject as? Feed {
|
||||
timeline.title = feed.nameForDisplay
|
||||
timeline.representedObjects = [feed]
|
||||
if let nameProvider = node.representedObject as? DisplayNameProvider {
|
||||
timeline.title = nameProvider.nameForDisplay
|
||||
}
|
||||
|
||||
timeline.appModelController = appModelController
|
||||
|
||||
self.navigationController?.pushViewController(timeline, animated: true)
|
||||
|
||||
}
|
||||
|
|
|
@ -13,18 +13,16 @@ import Articles
|
|||
|
||||
class MasterTimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
private var showAvatars = false
|
||||
private var rowHeightWithFeedName: CGFloat = 0.0
|
||||
private var rowHeightWithoutFeedName: CGFloat = 0.0
|
||||
|
||||
private var currentRowHeight: CGFloat {
|
||||
return showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName
|
||||
return appModelController.showFeedNames ? rowHeightWithFeedName : rowHeightWithoutFeedName
|
||||
}
|
||||
|
||||
static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5)
|
||||
|
||||
var appModelController: AppModelController!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
|
||||
var detailViewController: DetailViewController? {
|
||||
if let split = splitViewController {
|
||||
let controllers = split.viewControllers
|
||||
|
@ -32,69 +30,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var representedObjects: [AnyObject]? {
|
||||
didSet {
|
||||
if !representedObjectArraysAreEqual(oldValue, representedObjects) {
|
||||
|
||||
if let representedObjects = representedObjects {
|
||||
if representedObjects.count == 1 && representedObjects.first is Feed {
|
||||
showFeedNames = false
|
||||
}
|
||||
else {
|
||||
showFeedNames = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
showFeedNames = false
|
||||
}
|
||||
|
||||
fetchArticles()
|
||||
if articles.count > 0 {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var articles = ArticleArray() {
|
||||
didSet {
|
||||
if articles == oldValue {
|
||||
return
|
||||
}
|
||||
if articles.representSameArticlesInSameOrder(as: oldValue) {
|
||||
// When the array is the same — same articles, same order —
|
||||
// but some data in some of the articles may have changed.
|
||||
// Just reload visible cells in this case: don’t call reloadData.
|
||||
articleRowMap = [String: Int]()
|
||||
reloadAllVisibleCells()
|
||||
return
|
||||
}
|
||||
updateShowAvatars()
|
||||
articleRowMap = [String: Int]()
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private var articleRowMap = [String: Int]() // articleID: rowIndex
|
||||
private var showFeedNames = false {
|
||||
didSet {
|
||||
if showFeedNames != oldValue {
|
||||
updateShowAvatars()
|
||||
updateTableViewRowHeight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sortDirection = AppDefaults.timelineSortDirection {
|
||||
didSet {
|
||||
if sortDirection != oldValue {
|
||||
sortDirectionDidChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
@ -109,10 +45,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: appModelController)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(showFeedNamesDidChange(_:)), name: .ShowFeedNamesDidChange, object: appModelController)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: appModelController)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articlesDidChange(_:)), name: .ArticlesDidChange, object: appModelController)
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
|
||||
|
@ -132,7 +71,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
if segue.identifier == "showDetail" {
|
||||
if let indexPath = tableView.indexPathForSelectedRow {
|
||||
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
|
||||
let article = articles[indexPath.row]
|
||||
let article = appModelController.articles[indexPath.row]
|
||||
controller.article = article
|
||||
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
|
||||
controller.navigationItem.leftItemsSupplementBackButton = true
|
||||
|
@ -156,7 +95,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?.articles,
|
||||
guard let articles = self?.appModelController.articles,
|
||||
let undoManager = self?.undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else {
|
||||
return
|
||||
|
@ -177,12 +116,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return articles.count
|
||||
return appModelController.articles.count
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
let article = articles[indexPath.row]
|
||||
let article = appModelController.articles[indexPath.row]
|
||||
|
||||
// Set up the star action
|
||||
let starTitle = article.status.starred ?
|
||||
|
@ -226,7 +165,7 @@ 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 = articles[indexPath.row]
|
||||
let article = appModelController.articles[indexPath.row]
|
||||
|
||||
configureTimelineCell(cell, article: article)
|
||||
|
||||
|
@ -234,7 +173,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let article = articles[indexPath.row]
|
||||
let article = appModelController.articles[indexPath.row]
|
||||
if !article.status.read {
|
||||
markArticles(Set([article]), statusKey: .read, flag: true)
|
||||
}
|
||||
|
@ -267,7 +206,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = articles.articleAtRow(indexPath.row) else {
|
||||
guard let article = appModelController.articles.articleAtRow(indexPath.row) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -283,14 +222,14 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
|
||||
@objc func avatarDidBecomeAvailable(_ note: Notification) {
|
||||
|
||||
guard showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
guard appModelController.showAvatars, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
performBlockAndRestoreSelection {
|
||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||
|
||||
guard let article = articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
guard let article = appModelController.articles.articleAtRow(indexPath.row), let authors = article.authors, !authors.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -306,27 +245,29 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
}
|
||||
|
||||
@objc func imageDidBecomeAvailable(_ note: Notification) {
|
||||
if showAvatars {
|
||||
if appModelController.showAvatars {
|
||||
queueReloadVisableCells()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds) || representedObjectsContainsAnyPseudoFeed()
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
@objc func articlesReinitialized(_ note: Notification) {
|
||||
if appModelController.articles.count > 0 {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
self.sortDirection = AppDefaults.timelineSortDirection
|
||||
@objc func showFeedNamesDidChange(_ note: Notification) {
|
||||
updateTableViewRowHeight()
|
||||
}
|
||||
|
||||
|
||||
@objc func articleDataDidChange(_ note: Notification) {
|
||||
reloadAllVisibleCells()
|
||||
}
|
||||
|
||||
@objc func articlesDidChange(_ note: Notification) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: Reloading
|
||||
|
||||
@objc func reloadAllVisibleCells() {
|
||||
|
@ -349,7 +290,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
if articleIDs.isEmpty {
|
||||
return
|
||||
}
|
||||
let indexes = indexesForArticleIDs(articleIDs)
|
||||
let indexes = appModelController.indexesForArticleIDs(articleIDs)
|
||||
reloadVisibleCells(for: indexes)
|
||||
}
|
||||
|
||||
|
@ -385,26 +326,6 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||
updateTableViewRowHeight()
|
||||
}
|
||||
|
||||
@objc func fetchAndMergeArticles() {
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
return
|
||||
}
|
||||
|
||||
var unsortedArticles = fetchUnsortedArticles(for: representedObjects)
|
||||
|
||||
// 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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
@ -423,13 +344,13 @@ private extension MasterTimelineViewController {
|
|||
}
|
||||
let featuredImage = featuredImageFor(article)
|
||||
|
||||
cell.cellData = MasterTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: showAvatars, featuredImage: featuredImage)
|
||||
cell.cellData = MasterTimelineCellData(article: article, showFeedName: appModelController.showFeedNames, feedName: article.feed?.nameForDisplay, avatar: avatar, showAvatar: appModelController.showAvatars, featuredImage: featuredImage)
|
||||
|
||||
}
|
||||
|
||||
func avatarFor(_ article: Article) -> UIImage? {
|
||||
|
||||
if !showAvatars {
|
||||
if !appModelController.showAvatars {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -468,73 +389,6 @@ private extension MasterTimelineViewController {
|
|||
tableView.rowHeight = currentRowHeight
|
||||
tableView.estimatedRowHeight = currentRowHeight
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func representedObjectArraysAreEqual(_ objects1: [AnyObject]?, _ objects2: [AnyObject]?) -> Bool {
|
||||
|
||||
if objects1 == nil && objects2 == nil {
|
||||
return true
|
||||
}
|
||||
guard let objects1 = objects1, let objects2 = objects2 else {
|
||||
return false
|
||||
}
|
||||
if objects1.count != objects2.count {
|
||||
return false
|
||||
}
|
||||
|
||||
var ix = 0
|
||||
for oneObject in objects1 {
|
||||
if oneObject !== objects2[ix] {
|
||||
return false
|
||||
}
|
||||
ix += 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Fetching Articles
|
||||
|
||||
func fetchArticles() {
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
emptyTheTimeline()
|
||||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = fetchUnsortedArticles(for: representedObjects)
|
||||
updateArticles(with: fetchedArticles)
|
||||
|
||||
}
|
||||
|
||||
func emptyTheTimeline() {
|
||||
if !articles.isEmpty {
|
||||
articles = [Article]()
|
||||
}
|
||||
}
|
||||
|
||||
func sortDirectionDidChange() {
|
||||
updateArticles(with: Set(articles))
|
||||
}
|
||||
|
||||
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
||||
let indexPaths = tableView.indexPathsForSelectedRows
|
||||
|
@ -543,107 +397,5 @@ private extension MasterTimelineViewController {
|
|||
self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
}
|
||||
}
|
||||
|
||||
func updateArticles(with unsortedArticles: Set<Article>) {
|
||||
|
||||
let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection)
|
||||
if articles != sortedArticles {
|
||||
articles = sortedArticles
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func fetchUnsortedArticles(for representedObjects: [Any]) -> Set<Article> {
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
|
||||
for object in representedObjects {
|
||||
|
||||
if let articleFetcher = object as? ArticleFetcher {
|
||||
fetchedArticles.formUnion(articleFetcher.fetchArticles())
|
||||
}
|
||||
}
|
||||
|
||||
return fetchedArticles
|
||||
}
|
||||
|
||||
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 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() {
|
||||
MasterTimelineViewController.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles))
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyPseudoFeed() -> Bool {
|
||||
guard let representedObjects = representedObjects else {
|
||||
return false
|
||||
}
|
||||
for representedObject in representedObjects {
|
||||
if representedObject is PseudoFeed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
return false
|
||||
}
|
||||
for representedObject in representedObjects {
|
||||
if let feed = representedObject as? Feed {
|
||||
for oneFeed in feeds {
|
||||
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let folder = representedObject as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue