implement model controller pattern

This commit is contained in:
Maurice Parker 2019-04-21 13:57:23 -05:00
parent 17d83928a9
commit c5a891234d
5 changed files with 296 additions and 301 deletions

View File

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

View File

@ -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 */,

View File

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

View File

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

View File

@ -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: dont 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 theres 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
}
}