From 298e16474078cf7f4ed08b0a65140a84ccbd8445 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 1 Mar 2022 14:53:43 -0600 Subject: [PATCH 01/18] Change sync to async --- iOS/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index aeb74260e..da4a43fee 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -411,7 +411,7 @@ private extension AppDelegate { // set expiration handler task.expirationHandler = { [weak task] in os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info) - DispatchQueue.main.sync { + DispatchQueue.main.async { self.suspendApplication() task?.setTaskCompleted(success: false) } From 050bb67d80c3a2fe288f906b598ebfb11a3c8d3c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Fri, 25 Mar 2022 13:35:12 -0500 Subject: [PATCH 02/18] Updated to latest package versions --- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index dc8075713..f107850d7 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/microsoft/plcrashreporter.git", "state": { "branch": null, - "revision": "d747ab5de269cd44022bbe96ff9609d8626694ab", - "version": "1.9.0" + "revision": "6b27393cad517c067dceea85fadf050e70c4ceaa", + "version": "1.10.1" } }, { @@ -105,8 +105,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/Sparkle-Binary.git", "state": { "branch": null, - "revision": "67cd26321bdf4e77954cf6de7d9e6a20544f2030", - "version": "2.0.0" + "revision": "d1a8b3c98d96c601453f2e4230f1dd65b60d0581", + "version": "2.0.1" } }, { From f68836fd74e7f9abb463fca9205565543f812e01 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 26 Mar 2022 13:32:52 -0500 Subject: [PATCH 03/18] Add a commit to make sure the database has uncommitted data for the following query. --- SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift index 0ab0d80cb..286b50b11 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift @@ -30,6 +30,8 @@ struct SyncStatusTable: DatabaseTable { let updateSQL = "update syncStatus set selected = true" database.executeUpdate(updateSQL, withArgumentsIn: nil) + database.commit() + var selectSQL = "select * from syncStatus where selected == true" if let limit = limit { selectSQL = "\(selectSQL) limit \(limit)" From 671a402845e2a242c5e38182aedb1bc94f86707b Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 26 Mar 2022 16:15:43 -0500 Subject: [PATCH 04/18] Back out unnecessary commit statement. --- SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift index 286b50b11..0ab0d80cb 100644 --- a/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift +++ b/SyncDatabase/Sources/SyncDatabase/SyncStatusTable.swift @@ -30,8 +30,6 @@ struct SyncStatusTable: DatabaseTable { let updateSQL = "update syncStatus set selected = true" database.executeUpdate(updateSQL, withArgumentsIn: nil) - database.commit() - var selectSQL = "select * from syncStatus where selected == true" if let limit = limit { selectSQL = "\(selectSQL) limit \(limit)" From fdadb8086360698f7d64931fe6fa71b754e857ad Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 6 Apr 2022 21:52:45 -0700 Subject: [PATCH 05/18] Update build number and write release notes for 6104 TestFlight beta. --- Technotes/ReleaseNotes-iOS.markdown | 8 ++++++++ xcconfig/common/NetNewsWire_ios_target_common.xcconfig | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Technotes/ReleaseNotes-iOS.markdown b/Technotes/ReleaseNotes-iOS.markdown index d0db5db3a..a90c4f723 100644 --- a/Technotes/ReleaseNotes-iOS.markdown +++ b/Technotes/ReleaseNotes-iOS.markdown @@ -1,5 +1,13 @@ # iOS Release Notes +### 6.1 TestFligth build 6104 - 6 April 2022 + +Building on a new machine and making sure all’s well +Moved built-in themes to the app bundle so they’re always up to date +Fixed a crash in the Feeds list related to updating the feed image +Fixed a layout bug that could happen on returning to the Feeds list +Fixed a bug where go-to-feed might not properly expand disclosure triangles + ### 6.1 TestFlight build 6103 - 25 Jan 2022 Fixed regression with keyboard shortcuts. diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 4dbd4ef00..4351c5e70 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 = 6.1 -CURRENT_PROJECT_VERSION = 6103 +CURRENT_PROJECT_VERSION = 6104 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon From a0e8ca00eec377ca54159a1030cabf8a87c883cf Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Thu, 12 May 2022 21:56:49 -0700 Subject: [PATCH 06/18] =?UTF-8?q?Fix=20one=20of=20the=20causes=20of=20watc?= =?UTF-8?q?hdog=20crashes=20in=20WidgetDataEncoder=20by=20fetching=20just?= =?UTF-8?q?=20the=20count=20of=20starred=20articles=20=E2=80=94=20instead?= =?UTF-8?q?=20of=20fetching=20all=20the=20starred=20articles=20and=20count?= =?UTF-8?q?ing=20them,=20which=20can=20take=20a=20long=20time.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Account/Sources/Account/Account.swift | 4 + Account/Sources/Account/AccountManager.swift | 11 +++ .../ArticlesDatabase/ArticlesDatabase.swift | 4 + .../ArticlesDatabase/ArticlesTable.swift | 76 +++++++++++++++---- Shared/Widget/WidgetDataEncoder.swift | 2 +- 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index db7f23fd3..489182245 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -718,6 +718,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, database.fetchStarredAndUnreadCount(for: flattenedWebFeeds().webFeedIDs(), completion: completion) } + public func fetchCountForStarredArticles() throws -> Int { + return try database.fetchStarredArticlesCount(flattenedWebFeeds().webFeedIDs()) + } + public func fetchUnreadArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { database.fetchUnreadArticleIDsAsync(completion: completion) } diff --git a/Account/Sources/Account/AccountManager.swift b/Account/Sources/Account/AccountManager.swift index bc1911947..07bd893d9 100644 --- a/Account/Sources/Account/AccountManager.swift +++ b/Account/Sources/Account/AccountManager.swift @@ -393,6 +393,17 @@ public final class AccountManager: UnreadCountProvider { } } } + + // MARK: - Fetching Article Counts + + public func fetchCountForStarredArticles() throws -> Int { + precondition(Thread.isMainThread) + var count = 0 + for account in activeAccounts { + count += try account.fetchCountForStarredArticles() + } + return count + } // MARK: - Caches diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift index 5a7355201..fae1df9b2 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesDatabase.swift @@ -114,6 +114,10 @@ public final class ArticlesDatabase { return try articlesTable.fetchStarredArticles(webFeedIDs, limit) } + public func fetchStarredArticlesCount(_ webFeedIDs: Set) throws -> Int { + return try articlesTable.fetchStarredArticlesCount(webFeedIDs) + } + public func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set) throws -> Set
{ return try articlesTable.fetchArticlesMatching(searchString, webFeedIDs) } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift index ab7655474..a64be83dc 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/ArticlesTable.swift @@ -32,7 +32,8 @@ final class ArticlesTable: DatabaseTable { let articleCutoffDate = Date().bySubtracting(days: 90) private typealias ArticlesFetchMethod = (FMDatabase) -> Set
- + private typealias ArticlesCountFetchMethod = (FMDatabase) -> Int + init(name: String, accountID: String, queue: DatabaseQueue, retentionStyle: ArticlesDatabase.RetentionStyle) { self.name = name @@ -103,6 +104,10 @@ final class ArticlesTable: DatabaseTable { fetchArticlesAsync({ self.fetchStarredArticles(webFeedIDs, limit, $0) }, completion) } + func fetchStarredArticlesCount(_ webFeedIDs: Set) throws -> Int { + return try fetchArticlesCount{ self.fetchStarredArticlesCount(webFeedIDs, $0) } + } + // MARK: - Fetching Search Articles func fetchArticlesMatching(_ searchString: String) throws -> Set
{ @@ -671,6 +676,23 @@ private extension ArticlesTable { return articles } + private func fetchArticlesCount(_ fetchMethod: @escaping ArticlesCountFetchMethod) throws -> Int { + var articlesCount = 0 + var error: DatabaseError? = nil + queue.runInDatabaseSync { databaseResult in + switch databaseResult { + case .success(let database): + articlesCount = fetchMethod(database) + case .failure(let databaseError): + error = databaseError + } + } + if let error = error { + throw(error) + } + return articlesCount + } + private func fetchArticlesAsync(_ fetchMethod: @escaping ArticlesFetchMethod, _ completion: @escaping ArticleSetResultBlock) { queue.runInDatabase { databaseResult in @@ -745,6 +767,19 @@ private extension ArticlesTable { return articlesWithSQL(sql, parameters, database) } + func fetchArticleCountsWithWhereClause(_ database: FMDatabase, whereClause: String, parameters: [AnyObject]) -> Int { + let sql = "select count(*) from articles natural join statuses where \(whereClause);" + guard let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) else { + return 0 + } + var articlesCount = 0 + if resultSet.next() { + articlesCount = resultSet.long(forColumnIndex: 0) + } + resultSet.close() + return articlesCount + } + func fetchArticlesMatching(_ searchString: String, _ database: FMDatabase) -> Set
{ let sql = "select rowid from search where search match ?;" let sqlSearchString = sqliteSearchString(with: searchString) @@ -840,20 +875,31 @@ private extension ArticlesTable { return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) } - func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ - // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; - if webFeedIDs.isEmpty { - return Set
() - } - let parameters = webFeedIDs.map { $0 as AnyObject } - let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! - var whereClause = "feedID in \(placeholders) and starred=1" - if let limit = limit { - whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") - } - return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) - } - + func fetchStarredArticles(_ webFeedIDs: Set, _ limit: Int?, _ database: FMDatabase) -> Set
{ + // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; + if webFeedIDs.isEmpty { + return Set
() + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + var whereClause = "feedID in \(placeholders) and starred=1" + if let limit = limit { + whereClause.append(" order by coalesce(datePublished, dateModified, dateArrived) desc limit \(limit)") + } + return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters) + } + + func fetchStarredArticlesCount(_ webFeedIDs: Set, _ database: FMDatabase) -> Int { + // select count from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and starred=1; + if webFeedIDs.isEmpty { + return 0 + } + let parameters = webFeedIDs.map { $0 as AnyObject } + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(webFeedIDs.count))! + let whereClause = "feedID in \(placeholders) and starred=1" + return fetchArticleCountsWithWhereClause(database, whereClause: whereClause, parameters: parameters) + } + func fetchArticlesMatching(_ searchString: String, _ webFeedIDs: Set, _ database: FMDatabase) -> Set
{ let articles = fetchArticlesMatching(searchString, database) // TODO: include the feedIDs in the SQL rather than filtering here. diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 86c46be3b..db830eb8d 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -73,7 +73,7 @@ public final class WidgetDataEncoder { let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, - currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, + currentStarredCount: try AccountManager.shared.fetchCountForStarredArticles(), unreadArticles: unread, starredArticles: starred, todayArticles:today, From 66a5e42e8ad74dca7ac4ffaf09d30469b9724bc5 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 16 May 2022 09:32:38 +0800 Subject: [PATCH 07/18] Updates widget code --- Shared/Widget/WidgetDataEncoder.swift | 37 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index db830eb8d..3462892e4 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -23,13 +23,19 @@ public final class WidgetDataEncoder { private var backgroundTaskID: UIBackgroundTaskIdentifier! private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String private lazy var containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) + private lazy var imageContainer = containerURL?.appendingPathComponent("widgetImages", isDirectory: true) private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json") static let shared = WidgetDataEncoder() - private init () {} + private init () { + if imageContainer != nil { + try? FileManager.default.createDirectory(at: imageContainer!, withIntermediateDirectories: true, attributes: nil) + } + } @available(iOS 14, *) func encodeWidgetData() throws { + flushSharedContainer() os_log(.debug, log: log, "Starting encoding widget data.") do { @@ -46,7 +52,7 @@ public final class WidgetDataEncoder { feedTitle: article.sortableName, articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), + feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), pubDate: article.datePublished?.description ?? "") unread.append(latestArticle) } @@ -56,7 +62,7 @@ public final class WidgetDataEncoder { feedTitle: article.sortableName, articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), + feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), pubDate: article.datePublished?.description ?? "") starred.append(latestArticle) } @@ -66,7 +72,7 @@ public final class WidgetDataEncoder { feedTitle: article.sortableName, articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), + feedIconPath: writeImageDataToSharedContainer(article.iconImage()?.image.dataRepresentation()), pubDate: article.datePublished?.description ?? "") today.append(latestArticle) } @@ -113,5 +119,28 @@ public final class WidgetDataEncoder { FileManager.default.fileExists(atPath: dataURL!.path) } + private func writeImageDataToSharedContainer(_ imageData: Data?) -> String? { + if imageData == nil { return nil } + // Each image gets a UUID + let uuid = UUID().uuidString + if let imagePath = imageContainer?.appendingPathComponent(uuid, isDirectory: false) { + do { + try imageData!.write(to: imagePath) + return imagePath.path + } catch { + return nil + } + } + + return nil + } + + private func flushSharedContainer() { + if let imageContainer = imageContainer { + try? FileManager.default.removeItem(atPath: imageContainer.path) + try? FileManager.default.createDirectory(at: imageContainer, withIntermediateDirectories: true, attributes: nil) + } + } + } From 350c373ae2f7036cadb22c218f5062dc06266fff Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 7 Jul 2022 08:14:09 +0100 Subject: [PATCH 08/18] Fixes `LatestArticle` to use feedIconPath --- Shared/Widget/WidgetData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/Widget/WidgetData.swift b/Shared/Widget/WidgetData.swift index 1c4387a5a..5b6cf9655 100644 --- a/Shared/Widget/WidgetData.swift +++ b/Shared/Widget/WidgetData.swift @@ -26,7 +26,7 @@ struct LatestArticle: Codable, Identifiable { let feedTitle: String let articleTitle: String? let articleSummary: String? - let feedIcon: Data? // Base64 encoded image data + let feedIconPath: String? // Path to image data in shared container. let pubDate: String } From db9dbc97ecfe45d542489a2264e1d05817e5b6ff Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 7 Jul 2022 19:12:42 +0100 Subject: [PATCH 09/18] ArticleItemView now uses feedIconPath --- Widget/Shared Views/ArticleItemView.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Widget/Shared Views/ArticleItemView.swift b/Widget/Shared Views/ArticleItemView.swift index ff922155e..a3fbebba9 100644 --- a/Widget/Shared Views/ArticleItemView.swift +++ b/Widget/Shared Views/ArticleItemView.swift @@ -13,14 +13,14 @@ struct ArticleItemView: View { var article: LatestArticle var deepLink: URL - @State private var iconImage: UIImage? + @State private var iconImage: Image? var body: some View { Link(destination: deepLink, label: { HStack(alignment: .top, spacing: nil, content: { // Feed Icon if iconImage != nil { - Image(uiImage: iconImage!) + iconImage! .resizable() .frame(width: 30, height: 30) .cornerRadius(4) @@ -49,16 +49,18 @@ struct ArticleItemView: View { } }) }).onAppear { - iconImage = thumbnail(article.feedIcon) + iconImage = thumbnail(from: article.feedIconPath) } } - func thumbnail(_ data: Data?) -> UIImage { - if data == nil { - return UIImage(systemName: "globe")! - } else { - return UIImage(data: data!)! + func thumbnail(from path: String?) -> Image? { + guard let imagePath = path, + let url = URL(string: imagePath), + let data = try? Data(contentsOf: url), + let uiImage = UIImage(data: data) else { + return Image(uiImage: UIImage(systemName: "globe")!) } + return Image(uiImage: uiImage) } func pubDate(_ dateString: String) -> String { From 27a5556cdd2e2ac57fcf059d43cc5a269e0bb986 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 9 Jul 2022 10:21:24 -0700 Subject: [PATCH 10/18] Update build number and release notes. --- Technotes/ReleaseNotes-iOS.markdown | 7 ++++++- xcconfig/common/NetNewsWire_ios_target_common.xcconfig | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Technotes/ReleaseNotes-iOS.markdown b/Technotes/ReleaseNotes-iOS.markdown index a90c4f723..8b216dffd 100644 --- a/Technotes/ReleaseNotes-iOS.markdown +++ b/Technotes/ReleaseNotes-iOS.markdown @@ -1,6 +1,11 @@ # iOS Release Notes -### 6.1 TestFligth build 6104 - 6 April 2022 +### 6.1 TestFlight build 6105 = 6 July 2022 + +Write widget icons to the shared container +Make crashes slightly less likely when building up widget data + +### 6.1 TestFlight build 6104 - 6 April 2022 Building on a new machine and making sure all’s well Moved built-in themes to the app bundle so they’re always up to date diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index 4351c5e70..2f0e70824 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 = 6.1 -CURRENT_PROJECT_VERSION = 6104 +CURRENT_PROJECT_VERSION = 6105 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon From 424000975deb8749761fc40dc24775464a5e9dcb Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sat, 9 Jul 2022 18:50:07 +0100 Subject: [PATCH 11/18] switches URL to `fileURLWithPath` --- Widget/Shared Views/ArticleItemView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widget/Shared Views/ArticleItemView.swift b/Widget/Shared Views/ArticleItemView.swift index a3fbebba9..781ad2955 100644 --- a/Widget/Shared Views/ArticleItemView.swift +++ b/Widget/Shared Views/ArticleItemView.swift @@ -55,7 +55,7 @@ struct ArticleItemView: View { func thumbnail(from path: String?) -> Image? { guard let imagePath = path, - let url = URL(string: imagePath), + let url = URL(fileURLWithPath: imagePath), let data = try? Data(contentsOf: url), let uiImage = UIImage(data: data) else { return Image(uiImage: UIImage(systemName: "globe")!) From 0bbb61511feee381b6f933c41dc9f2abf24cf3e2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 28 Jul 2022 16:19:02 -0500 Subject: [PATCH 12/18] Revert "switches URL to `fileURLWithPath`" --- Widget/Shared Views/ArticleItemView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Widget/Shared Views/ArticleItemView.swift b/Widget/Shared Views/ArticleItemView.swift index 781ad2955..a3fbebba9 100644 --- a/Widget/Shared Views/ArticleItemView.swift +++ b/Widget/Shared Views/ArticleItemView.swift @@ -55,7 +55,7 @@ struct ArticleItemView: View { func thumbnail(from path: String?) -> Image? { guard let imagePath = path, - let url = URL(fileURLWithPath: imagePath), + let url = URL(string: imagePath), let data = try? Data(contentsOf: url), let uiImage = UIImage(data: data) else { return Image(uiImage: UIImage(systemName: "globe")!) From 141ed4f915d35935130f597f3da754533aa47d80 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 28 Jul 2022 17:15:36 -0500 Subject: [PATCH 13/18] Write widget data when article status changes happen. Fixes #3567 --- Shared/Widget/WidgetDataEncoder.swift | 46 +++++++++++++++++++-------- iOS/AppDelegate.swift | 13 +++----- iOS/SceneDelegate.swift | 3 -- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 3462892e4..5276470f0 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -20,21 +20,48 @@ public final class WidgetDataEncoder { private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") private let fetchLimit = 7 - private var backgroundTaskID: UIBackgroundTaskIdentifier! private lazy var appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as! String private lazy var containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) private lazy var imageContainer = containerURL?.appendingPathComponent("widgetImages", isDirectory: true) private lazy var dataURL = containerURL?.appendingPathComponent("widget-data.json") - static let shared = WidgetDataEncoder() - private init () { + private let encodeWidgetDataQueue = CoalescingQueue(name: "Encode the Widget Data", interval: 5.0) + + init () { if imageContainer != nil { try? FileManager.default.createDirectory(at: imageContainer!, withIntermediateDirectories: true, attributes: nil) } + if #available(iOS 14, *) { + NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) + } + } + + func encodeIfNecessary() { + encodeWidgetDataQueue.performCallsImmediately() + } + + @available(iOS 14, *) + @objc func statusesDidChange(_ note: Notification) { + encodeWidgetDataQueue.add(self, #selector(performEncodeWidgetData)) + } + + @available(iOS 14, *) + @objc private func performEncodeWidgetData() { + // We will be on the Main Thread when the encodeIfNecessary function is called. We want + // block the main thread in that case so that the widget data is encoded. If it is on + // a background Thread, it was called by the CoalescingQueue. In that case we need to + // move it to the Main Thread and want to execute it async. + if Thread.isMainThread { + encodeWidgetData() + } else { + DispatchQueue.main.async { + self.encodeWidgetData() + } + } } @available(iOS 14, *) - func encodeWidgetData() throws { + private func encodeWidgetData() { flushSharedContainer() os_log(.debug, log: log, "Starting encoding widget data.") @@ -89,10 +116,6 @@ public final class WidgetDataEncoder { DispatchQueue.global().async { [weak self] in guard let self = self else { return } - self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } let encodedData = try? JSONEncoder().encode(latestData) os_log(.debug, log: self.log, "Finished encoding widget data.") @@ -104,14 +127,11 @@ public final class WidgetDataEncoder { if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { os_log(.debug, log: self.log, "Wrote widget data to container.") WidgetCenter.shared.reloadAllTimelines() - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } else { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid } } + } catch { + os_log(.error, log: log, "WidgetDataEncoder failed to write the widget data.") } } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index da4a43fee..8ba3c7288 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -45,6 +45,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var webFeedIconDownloader: WebFeedIconDownloader! var extensionContainersFile: ExtensionContainersFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! + var widgetDataEncoder: WidgetDataEncoder! var unreadCount = 0 { didSet { @@ -114,6 +115,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() + widgetDataEncoder = WidgetDataEncoder() + syncTimer = ArticleStatusSyncTimer() #if DEBUG @@ -172,6 +175,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func prepareAccountsForBackground() { extensionFeedAddRequestFile.suspend() + widgetDataEncoder.encodeIfNecessary() syncTimer?.invalidate() scheduleBackgroundFeedRefresh() syncArticleStatus() @@ -398,9 +402,6 @@ private extension AppDelegate { } AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self.suspendApplication() os_log("Account refresh operation completed.", log: self.log, type: .info) task.setTaskCompleted(success: true) @@ -445,9 +446,6 @@ private extension AppDelegate { self.prepareAccountsForBackground() account!.syncArticleStatus(completion: { [weak self] _ in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self?.prepareAccountsForBackground() self?.suspendApplication() } @@ -474,9 +472,6 @@ private extension AppDelegate { account!.markArticles(article!, statusKey: .starred, flag: true) { _ in } account!.syncArticleStatus(completion: { [weak self] _ in if !AccountManager.shared.isSuspended { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } self?.prepareAccountsForBackground() self?.suspendApplication() } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 523446eed..7f5d14d3f 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -66,9 +66,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } func sceneDidEnterBackground(_ scene: UIScene) { - if #available(iOS 14, *) { - try? WidgetDataEncoder.shared.encodeWidgetData() - } ArticleStringFormatter.emptyCaches() appDelegate.prepareAccountsForBackground() } From 10a455399d708ae10a364ce2549d74ec274d53d8 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Fri, 29 Jul 2022 07:33:05 +0800 Subject: [PATCH 14/18] corrected ordering of guard for thumbnail --- Widget/Shared Views/ArticleItemView.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Widget/Shared Views/ArticleItemView.swift b/Widget/Shared Views/ArticleItemView.swift index a3fbebba9..d88a56932 100644 --- a/Widget/Shared Views/ArticleItemView.swift +++ b/Widget/Shared Views/ArticleItemView.swift @@ -54,13 +54,18 @@ struct ArticleItemView: View { } func thumbnail(from path: String?) -> Image? { - guard let imagePath = path, - let url = URL(string: imagePath), - let data = try? Data(contentsOf: url), - let uiImage = UIImage(data: data) else { - return Image(uiImage: UIImage(systemName: "globe")!) - } - return Image(uiImage: uiImage) + guard let imagePath = path else { + return Image(uiImage: UIImage(systemName: "globe")!) + } + + let url = URL(fileURLWithPath: imagePath) + + guard let data = try? Data(contentsOf: url), + let uiImage = UIImage(data: data) else { + return Image(uiImage: UIImage(systemName: "globe")!) + } + + return Image(uiImage: uiImage) } func pubDate(_ dateString: String) -> String { From 3a9e3c805012640ebc27db794821e263386a8e67 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 2 Aug 2022 18:10:09 -0500 Subject: [PATCH 15/18] Restrict the delete function if not the first responder. Fixes #3642 --- iOS/MasterFeed/MasterFeedViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 4e71c23ae..8dcaf04e0 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -523,6 +523,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(UIResponder.delete(_:)) { + return isFirstResponder + } + return super.canPerformAction(action, withSender: sender) + } + @objc func expandSelectedRows(_ sender: Any?) { if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) { coordinator.expand(node) From f52276f9a183035a4997afbf4ffc6980d61a0a84 Mon Sep 17 00:00:00 2001 From: Ethan Wong Date: Sun, 19 Jun 2022 11:57:10 +0800 Subject: [PATCH 16/18] use .package(path:) for local package dependencies to resolve Xcode 14 errors. --- Account/Package.swift | 34 ++++++++++++++++++++++++---------- ArticlesDatabase/Package.swift | 23 +++++++++++++++++------ SyncDatabase/Package.swift | 21 ++++++++++++++++----- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/Account/Package.swift b/Account/Package.swift index 040712530..5c6289899 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -1,6 +1,29 @@ // swift-tools-version:5.3 import PackageDescription +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), + .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), +] + +#if swift(>=5.6) +dependencies.append(contentsOf: [ + .package(path: "../Articles"), + .package(path: "../ArticlesDatabase"), + .package(path: "../Secrets"), + .package(path: "../SyncDatabase"), +]) +#else +dependencies.append(contentsOf: [ + .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), + .package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")), + .package(url: "../Secrets", .upToNextMajor(from: "1.0.0")), + .package(url: "../SyncDatabase", .upToNextMajor(from: "1.0.0")), +]) +#endif + let package = Package( name: "Account", platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)], @@ -10,16 +33,7 @@ let package = Package( type: .dynamic, targets: ["Account"]), ], - dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), - .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), - .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), - .package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")), - .package(url: "../Secrets", .upToNextMajor(from: "1.0.0")), - .package(url: "../SyncDatabase", .upToNextMajor(from: "1.0.0")), - ], + dependencies: dependencies, targets: [ .target( name: "Account", diff --git a/ArticlesDatabase/Package.swift b/ArticlesDatabase/Package.swift index 127979844..e47d036b5 100644 --- a/ArticlesDatabase/Package.swift +++ b/ArticlesDatabase/Package.swift @@ -3,6 +3,22 @@ import PackageDescription +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), +] + +#if swift(>=5.6) +dependencies.append(contentsOf: [ + .package(path: "../Articles"), +]) +#else +dependencies.append(contentsOf: [ + .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), +]) +#endif + let package = Package( name: "ArticlesDatabase", platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)], @@ -12,12 +28,7 @@ let package = Package( type: .dynamic, targets: ["ArticlesDatabase"]), ], - dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), - .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), - ], + dependencies: dependencies, targets: [ .target( name: "ArticlesDatabase", diff --git a/SyncDatabase/Package.swift b/SyncDatabase/Package.swift index 15f06261c..19f86b7ce 100644 --- a/SyncDatabase/Package.swift +++ b/SyncDatabase/Package.swift @@ -1,6 +1,21 @@ // swift-tools-version:5.3 import PackageDescription +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), +] + +#if swift(>=5.6) +dependencies.append(contentsOf: [ + .package(path: "../Articles"), +]) +#else +dependencies.append(contentsOf: [ + .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), +]) +#endif + let package = Package( name: "SyncDatabase", platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)], @@ -10,11 +25,7 @@ let package = Package( type: .dynamic, targets: ["SyncDatabase"]), ], - dependencies: [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "1.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")), - .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), - ], + dependencies: dependencies, targets: [ .target( name: "SyncDatabase", From 1a07d90dc2668ef5d7fb9b26046b3d67f8362b5f Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 17 Sep 2022 14:05:40 -0500 Subject: [PATCH 17/18] Update the build target so that we can compile with the more strict compiler in Xcode 14 --- xcconfig/NetNewsWire_project.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcconfig/NetNewsWire_project.xcconfig b/xcconfig/NetNewsWire_project.xcconfig index 465091c17..2a51a2d1d 100644 --- a/xcconfig/NetNewsWire_project.xcconfig +++ b/xcconfig/NetNewsWire_project.xcconfig @@ -40,7 +40,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_VARIABLE = YES MACOSX_DEPLOYMENT_TARGET = 10.15 -IPHONEOS_DEPLOYMENT_TARGET = 13.0 +IPHONEOS_DEPLOYMENT_TARGET = 13.4 //SDKROOT = macosx SWIFT_SWIFT3_OBJC_INFERENCE = Off SWIFT_VERSION = 5.1 From b92fceb84e3d1e16716c2e7e16d543e1bbcef724 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 22 Sep 2022 20:25:03 -0500 Subject: [PATCH 18/18] Add links to iCloud Syncing Limitations & Solutions --- .../CloudKit/CloudKitWebDocumentation.swift | 12 +++ iOS/Account/Account.storyboard | 37 +++++--- .../CloudKitAccountViewController.swift | 9 +- .../AccountInspectorViewController.swift | 13 +++ iOS/Inspector/Inspector.storyboard | 94 ++++++++++++------- 5 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 Account/Sources/Account/CloudKit/CloudKitWebDocumentation.swift diff --git a/Account/Sources/Account/CloudKit/CloudKitWebDocumentation.swift b/Account/Sources/Account/CloudKit/CloudKitWebDocumentation.swift new file mode 100644 index 000000000..a55a494da --- /dev/null +++ b/Account/Sources/Account/CloudKit/CloudKitWebDocumentation.swift @@ -0,0 +1,12 @@ +// +// File.swift +// +// +// Created by Maurice Parker on 9/22/22. +// + +import Foundation + +public struct CloudKitWebDocumentation { + public static let limitationsAndSolutions = "https://netnewswire.com/help/iCloud" +} diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard index 9f9b4def0..cf6e9761e 100644 --- a/iOS/Account/Account.storyboard +++ b/iOS/Account/Account.storyboard @@ -1,9 +1,9 @@ - + - + @@ -14,7 +14,7 @@ - + @@ -30,7 +30,7 @@ - + @@ -144,7 +144,7 @@ Don’t have a Feed Wrangler account? + + @@ -907,7 +918,7 @@ Don’t have a Reader account? + + + + + + + - + - + - + - + @@ -45,14 +65,14 @@ - + - +