diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 7fd30a705..ef8b4d94d 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -216,7 +216,6 @@ 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; 519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */; }; 519ED47A24482AEB007F8E94 /* EnableExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */; }; - 519ED47C24488C6F007F8E94 /* ExtensionInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */; }; 51A052CE244FB9D7006C2024 /* AddFeedWIndowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */; }; 51A16999235E10D700EB091F /* LocalAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */; }; 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51A16990235E10D600EB091F /* Settings.storyboard */; }; @@ -911,7 +910,6 @@ 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = ""; }; 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointViewController.swift; sourceTree = ""; }; - 519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionInspectorViewController.swift; sourceTree = ""; }; 51A052CD244FB9D6006C2024 /* AddFeedWIndowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedWIndowController.swift; path = AddFeed/AddFeedWIndowController.swift; sourceTree = ""; }; 51A1698F235E10D600EB091F /* LocalAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalAccountViewController.swift; sourceTree = ""; }; 51A16990235E10D600EB091F /* Settings.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; @@ -1425,7 +1423,6 @@ children = ( 516A09412361248000EAE89B /* Inspector.storyboard */, 51A16991235E10D600EB091F /* AccountInspectorViewController.swift */, - 519ED47B24488C6F007F8E94 /* ExtensionInspectorViewController.swift */, 5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */, 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */, ); @@ -3199,7 +3196,6 @@ 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */, 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */, - 519ED47C24488C6F007F8E94 /* ExtensionInspectorViewController.swift in Sources */, 51C4CFF224D37D1F00AF9874 /* Secrets.swift in Sources */, 51C452A022650A1900C03939 /* WebFeedIconDownloader.swift in Sources */, 51C4529E22650A1900C03939 /* ImageDownloader.swift in Sources */, diff --git a/iOS/Account/CloudKitAccountViewController.swift b/iOS/Account/CloudKitAccountViewController.swift index a16bc63d0..e6060a4c5 100644 --- a/iOS/Account/CloudKitAccountViewController.swift +++ b/iOS/Account/CloudKitAccountViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SafariServices import Account enum CloudKitAccountViewControllerError: LocalizedError { @@ -30,7 +31,7 @@ class CloudKitAccountViewController: UITableViewController { } private func setupFooter() { - footerLabel.text = NSLocalizedString("Feeds in your iCloud account will be synced across your Mac and iOS devices.\n\nImportant note: while NetNewsWire itself is very fast, iCloud syncing is sometimes very slow. This can happen after adding a number of feeds and when setting it up on a new device.\n\nIf that happens to you, it may appear stuck. But don’t worry — it’s not. Just let it run.", comment: "iCloud") + footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud") } @IBAction func cancel(_ sender: Any) { @@ -62,5 +63,10 @@ class CloudKitAccountViewController: UITableViewController { return super.tableView(tableView, viewForHeaderInSection: section) } } - + + @IBAction func openLimitationsAndSolutions(_ sender: Any) { + let vc = SFSafariViewController(url: CloudKitWebDocumentation.limitationsAndSolutionsURL) + vc.modalPresentationStyle = .pageSheet + present(vc, animated: true) + } } diff --git a/iOS/Add/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift index f5a38a1ca..2ecd326ea 100644 --- a/iOS/Add/AddFeedViewController.swift +++ b/iOS/Add/AddFeedViewController.swift @@ -12,10 +12,6 @@ import RSCore import RSTree import RSParser -enum AddFeedType { - case web -} - class AddFeedViewController: UITableViewController { @IBOutlet weak var activityIndicator: UIActivityIndicatorView! @@ -37,12 +33,7 @@ class AddFeedViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - - switch addFeedType { - default: - break - } - + activityIndicator.isHidden = true activityIndicator.color = .label diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index c7a01f33a..8fc8e11ae 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -28,7 +28,7 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { final class AppDefaults { - static let defaultThemeName = "Defaults" + static let defaultThemeName = "Default" static let shared = AppDefaults() private init() {} @@ -41,7 +41,6 @@ final class AppDefaults { struct Key { static let userInterfaceColorPalette = "userInterfaceColorPalette" - static let activeExtensionPointIDs = "activeExtensionPointIDs" static let lastImageCacheFlushDate = "lastImageCacheFlushDate" static let firstRunDate = "firstRunDate" static let timelineGroupByFeed = "timelineGroupByFeed" @@ -114,15 +113,6 @@ final class AppDefaults { } } - var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? { - get { - return UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] - } - set { - UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) - } - } - var useSystemBrowser: Bool { get { return UserDefaults.standard.bool(forKey: Key.useSystemBrowser) diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index f7cbe5b84..3bc35b786 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -45,7 +45,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD var webFeedIconDownloader: WebFeedIconDownloader! var extensionContainersFile: ExtensionContainersFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! - + var widgetDataEncoder: WidgetDataEncoder! + var unreadCount = 0 { didSet { if unreadCount != oldValue { @@ -72,8 +73,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath) - FeedProviderManager.shared.delegate = ExtensionPointManager.shared - NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil) } @@ -114,8 +113,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() + widgetDataEncoder = WidgetDataEncoder() + syncTimer = ArticleStatusSyncTimer() - + #if DEBUG syncTimer!.update() #endif @@ -175,9 +176,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD syncTimer?.invalidate() scheduleBackgroundFeedRefresh() syncArticleStatus() + widgetDataEncoder.encode() waitForSyncTasksToFinish() } - + func prepareAccountsForForeground() { extensionFeedAddRequestFile.resume() syncTimer?.update() @@ -293,7 +295,7 @@ private extension AppDelegate { return } - if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning { + if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning { os_log("Waiting for sync to finish...", log: self.log, type: .info) DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in self?.waitToComplete(completion: completion) @@ -387,9 +389,9 @@ private extension AppDelegate { /// - Parameter task: `BGAppRefreshTask` /// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work. func performBackgroundFeedRefresh(with task: BGAppRefreshTask) { - + scheduleBackgroundFeedRefresh() // schedule next refresh - + os_log("Woken to perform account refresh.", log: self.log, type: .info) DispatchQueue.main.async { @@ -398,26 +400,22 @@ 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) } } } - + // set expiration handler task.expirationHandler = { [weak task] in - DispatchQueue.main.sync { - self.suspendApplication() - } os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info) - task?.setTaskCompleted(success: false) + DispatchQueue.main.async { + self.suspendApplication() + task?.setTaskCompleted(success: false) + } } } - } // Handle Notification Actions @@ -445,9 +443,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 +469,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/Inspector/AccountInspectorViewController.swift b/iOS/Inspector/AccountInspectorViewController.swift index 681852eb5..6a8366f48 100644 --- a/iOS/Inspector/AccountInspectorViewController.swift +++ b/iOS/Inspector/AccountInspectorViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SafariServices import Account class AccountInspectorViewController: UITableViewController { @@ -16,7 +17,8 @@ class AccountInspectorViewController: UITableViewController { @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var activeSwitch: UISwitch! @IBOutlet weak var deleteAccountButton: VibrantButton! - + @IBOutlet weak var limitationsAndSolutionsButton: UIButton! + var isModal = false weak var account: Account? @@ -36,6 +38,10 @@ class AccountInspectorViewController: UITableViewController { deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal) } + if account.type != .cloudKit { + limitationsAndSolutionsButton.isHidden = true + } + if isModal { let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) navigationItem.leftBarButtonItem = doneBarButtonItem @@ -115,6 +121,12 @@ class AccountInspectorViewController: UITableViewController { present(alertController, animated: true) } + + @IBAction func openLimitationsAndSolutions(_ sender: Any) { + let vc = SFSafariViewController(url: CloudKitWebDocumentation.limitationsAndSolutionsURL) + vc.modalPresentationStyle = .pageSheet + present(vc, animated: true) + } } // MARK: Table View diff --git a/iOS/Inspector/ExtensionInspectorViewController.swift b/iOS/Inspector/ExtensionInspectorViewController.swift deleted file mode 100644 index 79c2cc43c..000000000 --- a/iOS/Inspector/ExtensionInspectorViewController.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// ExtensionPointInspectorViewController.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 4/16/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class ExtensionPointInspectorViewController: UITableViewController { - - @IBOutlet weak var extensionDescription: UILabel! - var extensionPoint: ExtensionPoint? - - override func viewDidLoad() { - super.viewDidLoad() - guard let extensionPoint = extensionPoint else { return } - navigationItem.title = extensionPoint.title - extensionDescription.attributedText = extensionPoint.description - tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - } - - @IBAction func disable(_ sender: Any) { - guard let extensionPoint = extensionPoint else { return } - - let title = NSLocalizedString("Deactivate Extension", comment: "Deactivate Extension") - let extensionPointTypeTitle = extensionPoint.extensionPointID.extensionPointType.title - let message = NSLocalizedString("Are you sure you want to deactivate the \(extensionPointTypeTitle) extension “\(extensionPoint.title)”?", comment: "Deactivate text") - - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") - let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) - alertController.addAction(cancelAction) - - let markTitle = NSLocalizedString("Deactivate", comment: "Deactivate") - let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in - ExtensionPointManager.shared.deactivateExtensionPoint(extensionPoint.extensionPointID) - self?.navigationController?.popViewController(animated: true) - } - alertController.addAction(markAction) - alertController.preferredAction = markAction - - present(alertController, animated: true) - - } -} - -// MARK: Table View - -extension ExtensionPointInspectorViewController { - - override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) - } - - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let extensionPoint = extensionPoint else { return nil } - - if section == 0 { - let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView - headerView.imageView.image = extensionPoint.image - return headerView - } else { - return super.tableView(tableView, viewForHeaderInSection: section) - } - } - - override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { - if indexPath.section > 0 { - return true - } - return false - } - - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - } - -} - diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index 1de01f5ab..fe101ba8b 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -47,9 +47,12 @@ class KeyboardManager { static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand { let selector = NSSelectorFromString(action) - return UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on) + let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on) + if #available(iOS 15.0, *) { + keyCommand.wantsPriorityOverSystemBehavior = true + } + return keyCommand } - } private extension KeyboardManager { @@ -58,14 +61,18 @@ private extension KeyboardManager { guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil } let modifiers = createKeyModifierFlags(keyEntry: keyEntry) let action = keyEntry["action"] as! String - + if let title = keyEntry["title"] as? String { return KeyboardManager.createKeyCommand(title: title, action: action, input: input, modifiers: modifiers) } else { - return UIKeyCommand(input: input, modifierFlags: modifiers, action: NSSelectorFromString(action)) + let keyCommand = UIKeyCommand(input: input, modifierFlags: modifiers, action: NSSelectorFromString(action)) + if #available(iOS 15.0, *) { + keyCommand.wantsPriorityOverSystemBehavior = true + } + return keyCommand } } - + static func createKeyCommandInput(keyEntry: [String: Any]) -> String? { guard let key = keyEntry["key"] as? String else { return nil } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 23c545780..a64c160e9 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -72,7 +72,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(configureContextMenu(_:)), name: .ActiveExtensionPointsDidChange, object: nil) refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) @@ -504,6 +503,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) @@ -608,6 +614,14 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } + if let rowChanges = changes.rowChanges { + for rowChange in rowChanges { + if let reloads = rowChange.reloadIndexPaths, !reloads.isEmpty { + tableView.reloadRows(at: reloads, with: .none) + } + } + } + completion?() } @@ -635,7 +649,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { var menuItems: [UIAction] = [] - let addWebFeedActionTitle = NSLocalizedString("Add Web Feed", comment: "Add Web Feed") + let addWebFeedActionTitle = NSLocalizedString("Add Feed", comment: "Add Feed") let addWebFeedAction = UIAction(title: addWebFeedActionTitle, image: AppAssets.plus) { _ in self.coordinator.showAddWebFeed() } diff --git a/iOS/MasterFeed/ShadowTableChanges.swift b/iOS/MasterFeed/ShadowTableChanges.swift index dd8a12801..ad5e9a73f 100644 --- a/iOS/MasterFeed/ShadowTableChanges.swift +++ b/iOS/MasterFeed/ShadowTableChanges.swift @@ -9,11 +9,11 @@ import Foundation struct ShadowTableChanges { - + struct Move: Hashable { var from: Int var to: Int - + init(_ from: Int, _ to: Int) { self.from = from self.to = to @@ -21,38 +21,44 @@ struct ShadowTableChanges { } struct RowChanges { - + var section: Int var deletes: Set? var inserts: Set? + var reloads: Set? var moves: Set? - + var isEmpty: Bool { return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true) } - + var deleteIndexPaths: [IndexPath]? { guard let deletes = deletes else { return nil } return deletes.map { IndexPath(row: $0, section: section) } } - + var insertIndexPaths: [IndexPath]? { guard let inserts = inserts else { return nil } return inserts.map { IndexPath(row: $0, section: section) } } - + + var reloadIndexPaths: [IndexPath]? { + guard let reloads = reloads else { return nil } + return reloads.map { IndexPath(row: $0, section: section) } + } + var moveIndexPaths: [(IndexPath, IndexPath)]? { guard let moves = moves else { return nil } return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) } } - - init(section: Int, deletes: Set?, inserts: Set?, moves: Set?) { + + init(section: Int, deletes: Set?, inserts: Set?, reloads: Set?, moves: Set?) { self.section = section self.deletes = deletes self.inserts = inserts + self.reloads = reloads self.moves = moves } - } var deletes: Set? @@ -66,5 +72,4 @@ struct ShadowTableChanges { self.moves = moves self.rowChanges = rowChanges } - }