diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index f2c11aa1b..de98b328c 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -235,8 +235,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, private enum OperationName { static let FetchAllUnreadCounts = "FetchAllUnreadCounts" static let FetchFeedUnreadCount = "FetchFeedUnreadCount" + static let FetchUnreadCountsForFeeds = "FetchUnreadCountsForFeeds" } - private static let discardableOperationNames = [OperationName.FetchAllUnreadCounts, OperationName.FetchFeedUnreadCount] + private static let discardableOperationNames = [OperationName.FetchAllUnreadCounts, OperationName.FetchFeedUnreadCount, OperationName.FetchUnreadCountsForFeeds] init?(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) { switch type { @@ -622,27 +623,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func updateUnreadCounts(for webFeeds: Set, completion: VoidCompletionBlock? = nil) { - if webFeeds.isEmpty { - completion?() - return - } - - if webFeeds.count == 1, let feed = webFeeds.first { - fetchUnreadCount(feed, completion) - } - else { - database.fetchUnreadCounts(for: webFeeds.webFeedIDs()) { unreadCountDictionaryResult in - if let unreadCountDictionary = try? unreadCountDictionaryResult.get() { - for webFeed in webFeeds { - if let unreadCount = unreadCountDictionary[webFeed.webFeedID] { - webFeed.unreadCount = unreadCount - } - } - } - - completion?() - } - } + fetchUnreadCounts(for: webFeeds, completion: completion) } public func fetchArticles(_ fetchType: FetchType) throws -> Set
{ @@ -1246,6 +1227,25 @@ private extension Account { NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.articleIDs: articleIDs]) } + /// Fetch unread counts for zero or more feeds. + /// + /// Uses the most efficient method based on how many feeds were passed in. + func fetchUnreadCounts(for feeds: Set, completion: VoidCompletionBlock?) { + if feeds.isEmpty { + completion?() + return + } + if feeds.count == 1, let feed = feeds.first { + fetchUnreadCount(feed, completion) + } + else if feeds.count < 10 { + fetchUnreadCounts(feeds, completion) + } + else { + fetchAllUnreadCounts() + } + } + func fetchUnreadCount(_ feed: WebFeed, _ completion: VoidCompletionBlock?) { let operation = database.createFetchFeedUnreadCountOperation(feedID: feed.webFeedID) operation.name = OperationName.FetchFeedUnreadCount @@ -1260,6 +1260,21 @@ private extension Account { operationQueue.addOperation(operation) } + func fetchUnreadCounts(_ feeds: Set, _ completion: VoidCompletionBlock?) { + let feedIDs = feeds.map { $0.webFeedID } + let operation = database.createFetchUnreadCountsForFeedsOperation(feedIDs: Set(feedIDs)) + operation.name = OperationName.FetchUnreadCountsForFeeds + operation.completionBlock = { operation in + let fetchOperation = operation as! FetchUnreadCountsForFeedsOperation + if let unreadCountDictionary = fetchOperation.unreadCountDictionary { + self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: feeds) + } + completion?() + } + + operationQueue.addOperation(operation) + } + func fetchAllUnreadCounts() { fetchingAllUnreadCounts = true operationQueue.cancelOperations(named: OperationName.FetchAllUnreadCounts) @@ -1271,7 +1286,7 @@ private extension Account { guard let unreadCountDictionary = fetchOperation.unreadCountDictionary else { return } - self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary) + self.processUnreadCounts(unreadCountDictionary: unreadCountDictionary, feeds: self.flattenedWebFeeds()) self.fetchingAllUnreadCounts = false self.updateUnreadCount() @@ -1282,8 +1297,8 @@ private extension Account { operationQueue.addOperation(operation) } - func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary) { - for feed in flattenedWebFeeds() { + func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set) { + for feed in feeds { // When the unread count is zero, it won’t appear in unreadCountDictionary. let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0 feed.unreadCount = unreadCount diff --git a/Frameworks/Account/FeedIdentifier.swift b/Frameworks/Account/FeedIdentifier.swift index 1bd3230d7..eed0a7555 100644 --- a/Frameworks/Account/FeedIdentifier.swift +++ b/Frameworks/Account/FeedIdentifier.swift @@ -79,5 +79,19 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable { return nil } } - + + // MARK: - Hashable + + public func hash(into hasher: inout Hasher) { + switch self { + case .smartFeed(let id): + hasher.combine(id) + case .script(let id): + hasher.combine(id) + case .webFeed(_, let webFeedID): + hasher.combine(webFeedID) + case .folder(_, let folderName): + hasher.combine(folderName) + } + } } diff --git a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index 62edf7a54..642b8b37c 100644 --- a/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Frameworks/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -84,6 +84,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl operationQueue.addOperation(addRequest) let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) + createFeeds.delegate = self createFeeds.addDependency(addRequest) createFeeds.downloadProgress = downloadProgress operationQueue.addOperation(createFeeds) @@ -91,17 +92,20 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, credentials: credentials, service: syncUnreadIdsService, database: database, newerThan: nil, log: log) syncUnread.addDependency(createFeeds) syncUnread.downloadProgress = downloadProgress + syncUnread.delegate = self operationQueue.addOperation(syncUnread) let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceId, service: getStreamContentsService, isPagingEnabled: false, newerThan: nil, log: log) syncFeed.addDependency(syncUnread) syncFeed.downloadProgress = downloadProgress + syncFeed.delegate = self operationQueue.addOperation(syncFeed) let finishOperation = FeedlyCheckpointOperation() finishOperation.checkpointDelegate = self finishOperation.downloadProgress = downloadProgress finishOperation.addDependency(syncFeed) + finishOperation.delegate = self operationQueue.addOperation(finishOperation) } @@ -109,6 +113,8 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl addCompletionHandler?(.failure(error)) addCompletionHandler = nil + os_log(.debug, log: log, "Unable to add new feed: %{public}@.", error as NSError) + cancel() } diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 9f6b34ba1..bb341bc4f 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -137,10 +137,6 @@ public final class ArticlesDatabase { // MARK: - Unread Counts - public func fetchUnreadCounts(for webFeedIDs: Set, _ completion: @escaping UnreadCountDictionaryCompletionBlock) { - articlesTable.fetchUnreadCounts(webFeedIDs, completion) - } - public func fetchUnreadCountForToday(for webFeedIDs: Set, completion: @escaping SingleUnreadCountCompletionBlock) { fetchUnreadCount(for: webFeedIDs, since: todayCutoffDate(), completion: completion) } @@ -203,6 +199,11 @@ public final class ArticlesDatabase { return FetchFeedUnreadCountOperation(feedID: feedID, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate) } + /// Create an operation that fetches unread counts for a number of feedIDs. + public func createFetchUnreadCountsForFeedsOperation(feedIDs: Set) -> FetchUnreadCountsForFeedsOperation { + return FetchUnreadCountsForFeedsOperation(feedIDs: feedIDs, databaseQueue: queue, cutoffDate: articlesTable.articleCutoffDate) + } + // MARK: - Suspend and Resume (for iOS) /// Close the database and stop running database calls. diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.xcodeproj/project.pbxproj b/Frameworks/ArticlesDatabase/ArticlesDatabase.xcodeproj/project.pbxproj index 7d1277606..055dd02a4 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.xcodeproj/project.pbxproj +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; }; 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; }; 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; }; + 84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */; }; 8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */; }; 848E3EB920FBCFD20004B7ED /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EB820FBCFD20004B7ED /* RSCore.framework */; }; 848E3EBD20FBCFDE0004B7ED /* RSDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848E3EBC20FBCFDE0004B7ED /* RSDatabase.framework */; }; @@ -126,6 +127,7 @@ 845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = ""; }; 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = ""; }; + 84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchUnreadCountsForFeedsOperation.swift; sourceTree = ""; }; 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 8477ACBB2221E76F00DF7F37 /* SearchTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTable.swift; sourceTree = ""; }; 848E3EB820FBCFD20004B7ED /* RSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -245,8 +247,9 @@ 84C242C723DEB42700C50516 /* Operations */ = { isa = PBXGroup; children = ( - 84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */, 84116B8823E01E86000B2E98 /* FetchFeedUnreadCountOperation.swift */, + 84611DCB23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift */, + 84C242C823DEB45C00C50516 /* FetchAllUnreadCountsOperation.swift */, ); path = Operations; sourceTree = ""; @@ -535,6 +538,7 @@ 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */, 8477ACBC2221E76F00DF7F37 /* SearchTable.swift in Sources */, + 84611DCC23E62FE200BC630C /* FetchUnreadCountsForFeedsOperation.swift in Sources */, 843702C31F70D15D00B18807 /* ParsedArticle+Database.swift in Sources */, 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */, 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */, diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 64cd7770a..1f0ab8414 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -237,31 +237,6 @@ final class ArticlesTable: DatabaseTable { // MARK: - Unread Counts - func fetchUnreadCounts(_ webFeedIDs: Set, _ completion: @escaping UnreadCountDictionaryCompletionBlock) { - if webFeedIDs.isEmpty { - completion(.success(UnreadCountDictionary())) - return - } - - fetchAllUnreadCounts { (unreadCountsResult) in - - func createUnreadCountDictionary(_ unreadCountDictionary: UnreadCountDictionary) -> UnreadCountDictionary { - var d = UnreadCountDictionary() - for webFeedID in webFeedIDs { - d[webFeedID] = unreadCountDictionary[webFeedID] ?? 0 - } - return d - } - - switch unreadCountsResult { - case .success(let unreadCountDictionary): - completion(.success(createUnreadCountDictionary(unreadCountDictionary))) - case .failure(let databaseError): - completion(.failure(databaseError)) - } - } - } - func fetchUnreadCount(_ webFeedIDs: Set, _ since: Date, _ completion: @escaping SingleUnreadCountCompletionBlock) { // Get unread count for today, for instance. if webFeedIDs.isEmpty { @@ -298,46 +273,6 @@ final class ArticlesTable: DatabaseTable { } } - func fetchAllUnreadCounts(_ completion: @escaping UnreadCountDictionaryCompletionBlock) { - // Returns only where unreadCount > 0. - - let cutoffDate = articleCutoffDate - queue.runInDatabase { databaseResult in - - func makeDatabaseCalls(_ database: FMDatabase) { - let sql = "select distinct feedID, count(*) from articles natural join statuses where read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;" - - guard let resultSet = database.executeQuery(sql, withArgumentsIn: [cutoffDate]) else { - DispatchQueue.main.async { - completion(.success(UnreadCountDictionary())) - } - return - } - - var d = UnreadCountDictionary() - while resultSet.next() { - let unreadCount = resultSet.long(forColumnIndex: 1) - if let webFeedID = resultSet.string(forColumnIndex: 0) { - d[webFeedID] = unreadCount - } - } - - DispatchQueue.main.async { - completion(.success(d)) - } - } - - switch databaseResult { - case .success(let database): - makeDatabaseCalls(database) - case .failure(let databaseError): - DispatchQueue.main.async { - completion(.failure(databaseError)) - } - } - } - } - func fetchStarredAndUnreadCount(_ webFeedIDs: Set, _ completion: @escaping SingleUnreadCountCompletionBlock) { if webFeedIDs.isEmpty { completion(.success(0)) @@ -630,15 +565,15 @@ private extension ArticlesTable { } } - func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int { - // Count only the articles that would appear in the UI. - // * Must be unread. - // * Must not be deleted. - // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date. - - let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0 and (starred=1 or dateArrived>?);" - return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database) - } +// func fetchUnreadCount(_ webFeedID: String, _ database: FMDatabase) -> Int { +// // Count only the articles that would appear in the UI. +// // * Must be unread. +// // * Must not be deleted. +// // * Must be either 1) starred or 2) dateArrived must be newer than cutoff date. +// +// let sql = "select count(*) from articles natural join statuses where feedID=? and read=0 and userDeleted=0 and (starred=1 or dateArrived>?);" +// return numberWithSQLAndParameters(sql, [webFeedID, articleCutoffDate], in: database) +// } func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set
{ let sql = "select rowid from search where search match ?;" diff --git a/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift b/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift new file mode 100644 index 000000000..38b2c9d22 --- /dev/null +++ b/Frameworks/ArticlesDatabase/Operations/FetchUnreadCountsForFeedsOperation.swift @@ -0,0 +1,88 @@ +// +// FetchUnreadCountsForFeedsOperation.swift +// ArticlesDatabase +// +// Created by Brent Simmons on 2/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import RSCore +import RSDatabase + +/// Fetch the unread counts for a number of feeds. +public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation { + + public var unreadCountDictionary: UnreadCountDictionary? + public let feedIDs: Set + + // MainThreadOperation + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private let queue: DatabaseQueue + private let cutoffDate: Date + + init(feedIDs: Set, databaseQueue: DatabaseQueue, cutoffDate: Date) { + self.feedIDs = feedIDs + self.queue = databaseQueue + self.cutoffDate = cutoffDate + } + + public func run() { + queue.runInDatabase { databaseResult in + if self.isCanceled { + self.informOperationDelegateOfCompletion() + return + } + + switch databaseResult { + case .success(let database): + self.fetchUnreadCounts(database) + case .failure: + self.informOperationDelegateOfCompletion() + } + } + } +} + +private extension FetchUnreadCountsForFeedsOperation { + + func fetchUnreadCounts(_ database: FMDatabase) { + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! + let sql = "select distinct feedID, count(*) from articles natural join statuses where feedID in \(placeholders) and read=0 and userDeleted=0 and (starred=1 or dateArrived>?) group by feedID;" + + var parameters = [Any]() + parameters += Array(feedIDs) as [Any] + parameters += [cutoffDate] as [Any] + + guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { + informOperationDelegateOfCompletion() + return + } + if isCanceled { + informOperationDelegateOfCompletion() + return + } + + var d = UnreadCountDictionary() + while resultSet.next() { + if isCanceled { + resultSet.close() + informOperationDelegateOfCompletion() + return + } + let unreadCount = resultSet.long(forColumnIndex: 1) + if let webFeedID = resultSet.string(forColumnIndex: 0) { + d[webFeedID] = unreadCount + } + } + resultSet.close() + + unreadCountDictionary = d + informOperationDelegateOfCompletion() + } +} diff --git a/Shared/Favicons/FaviconDownloader.swift b/Shared/Favicons/FaviconDownloader.swift index eee115711..447012cd6 100644 --- a/Shared/Favicons/FaviconDownloader.swift +++ b/Shared/Favicons/FaviconDownloader.swift @@ -24,6 +24,7 @@ final class FaviconDownloader { private let diskCache: BinaryDiskCache private var singleFaviconDownloaderCache = [String: SingleFaviconDownloader]() // faviconURL: SingleFaviconDownloader private var remainingFaviconURLs = [String: ArraySlice]() // homePageURL: array of faviconURLs that haven't been checked yet + private var currentHomePageHasOnlyFaviconICO = false private var homePageToFaviconURLCache = [String: String]() //homePageURL: faviconURL private var homePageToFaviconURLCachePath: String @@ -131,20 +132,14 @@ final class FaviconDownloader { } findFaviconURLs(with: url) { (faviconURLs) in - var hasIcons = false - if let faviconURLs = faviconURLs { + self.currentHomePageHasOnlyFaviconICO = faviconURLs.count == 1 + if let firstIconURL = faviconURLs.first { - hasIcons = true let _ = self.favicon(with: firstIconURL, homePageURL: url) self.remainingFaviconURLs[url] = faviconURLs.dropFirst() } } - - if (!hasIcons) { - self.homePageURLsWithNoFaviconURLCache.insert(url) - self.homePageURLsWithNoFaviconURLCacheDirty = true - } } return nil @@ -167,6 +162,11 @@ final class FaviconDownloader { remainingFaviconURLs[homePageURL] = faviconURLs.dropFirst(); } else { remainingFaviconURLs[homePageURL] = nil + + if currentHomePageHasOnlyFaviconICO { + self.homePageURLsWithNoFaviconURLCache.insert(homePageURL) + self.homePageURLsWithNoFaviconURLCacheDirty = true + } } } return diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index df1ba37c7..1ea187b56 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -215,7 +215,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { headerView.addGestureRecognizer(tap) // Without this the swipe gesture registers on the cell below - headerView.addGestureRecognizer(UIPanGestureRecognizer(target: nil, action: nil)) + let gestureRecognizer = UIPanGestureRecognizer(target: nil, action: nil) + gestureRecognizer.delegate = self + headerView.addGestureRecognizer(gestureRecognizer) headerView.interactions.removeAll() if section != 0 { @@ -1229,3 +1231,13 @@ private extension MasterFeedViewController { } } + +extension MasterFeedViewController: UIGestureRecognizerDelegate { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + let velocity = gestureRecognizer.velocity(in: self.view) + return abs(velocity.x) > abs(velocity.y); + } +} diff --git a/iOS/MasterFeed/RefreshProgressView.swift b/iOS/MasterFeed/RefreshProgressView.swift index 09b9f6574..6a627e10a 100644 --- a/iOS/MasterFeed/RefreshProgressView.swift +++ b/iOS/MasterFeed/RefreshProgressView.swift @@ -13,7 +13,7 @@ class RefreshProgressView: UIView { @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var label: UILabel! - private lazy var progressWidth = progressView.widthAnchor.constraint(equalToConstant: 100.0) + private lazy var progressWidthConstraint = progressView.widthAnchor.constraint(equalToConstant: 100.0) override func awakeFromNib() { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) @@ -28,6 +28,10 @@ class RefreshProgressView: UIView { scheduleUpdateRefreshLabel() } + override func didMoveToSuperview() { + progressChanged() + } + func updateRefreshLabel() { if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime { @@ -71,28 +75,38 @@ class RefreshProgressView: UIView { private extension RefreshProgressView { func progressChanged() { + // Layout may crash if not in the view hierarchy. + // https://github.com/Ranchero-Software/NetNewsWire/issues/1764 + let isInViewHierarchy = self.superview != nil + let progress = AccountManager.shared.combinedRefreshProgress if progress.isComplete { - progressView.setProgress(1, animated: true) + if isInViewHierarchy { + progressView.setProgress(1, animated: true) + } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.updateRefreshLabel() self.label.isHidden = false self.progressView.isHidden = true - self.progressWidth.isActive = false - self.progressView.setProgress(0, animated: true) + self.progressWidthConstraint.isActive = false + if isInViewHierarchy { + self.progressView.setProgress(0, animated: true) + } } } else { label.isHidden = true progressView.isHidden = false - self.progressWidth.isActive = true - self.progressView.setNeedsLayout() - self.progressView.layoutIfNeeded() - let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks) - - // Don't let the progress bar go backwards unless we need to go back more than 25% - if percent > progressView.progress || progressView.progress - percent > 0.25 { - progressView.setProgress(percent, animated: true) + progressWidthConstraint.isActive = true + if isInViewHierarchy { + progressView.setNeedsLayout() + progressView.layoutIfNeeded() + let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks) + + // Don't let the progress bar go backwards unless we need to go back more than 25% + if percent > progressView.progress || progressView.progress - percent > 0.25 { + progressView.setProgress(percent, animated: true) + } } } } diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 25353e80e..66e7e9d82 100644 --- a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig @@ -1,7 +1,7 @@ // High Level Settings common to both the iOS application and any extensions we bundle with it MARKETING_VERSION = 5.0 -CURRENT_PROJECT_VERSION = 33 +CURRENT_PROJECT_VERSION = 34 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon