From bbef99f2d38c4f0f77dcb533e2826fc506f68296 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Wed, 22 Jan 2025 22:18:09 -0800 Subject: [PATCH] Fix lint issues. --- .../CloudKitAccountViewController.swift | 18 +- .../FeedbinAccountViewController.swift | 53 +- iOS/Account/LocalAccountViewController.swift | 16 +- .../NewsBlurAccountViewController.swift | 8 +- .../ReaderAPIAccountViewController.swift | 51 +- iOS/Add/AddComboTableViewCell.swift | 6 +- iOS/Add/AddFeedFolderViewController.swift | 24 +- .../AddFeedSelectFolderTableViewCell.swift | 6 +- iOS/Add/AddFeedViewController.swift | 60 +- iOS/Add/AddFolderViewController.swift | 48 +- iOS/Add/SelectComboTableViewCell.swift | 8 +- iOS/AppAssets.swift | 73 ++- iOS/AppDefaults.swift | 41 +- iOS/AppDelegate.swift | 126 ++-- iOS/Article/ArticleExtractorButton.swift | 22 +- iOS/Article/ArticleSearchBar.swift | 51 +- .../ContextMenuPreviewViewController.swift | 16 +- iOS/Article/FindInArticleActivity.swift | 14 +- iOS/Article/ImageScrollView.swift | 163 +++-- iOS/Article/ImageTransition.swift | 32 +- iOS/Article/ImageViewController.swift | 29 +- iOS/Article/OpenInSafariActivity.swift | 16 +- iOS/Article/WebViewController.swift | 168 +++-- iOS/Article/WrapperScriptMessageHandler.swift | 8 +- iOS/ArticleActivityItemSource.swift | 14 +- iOS/ErrorHandler.swift | 4 +- iOS/IconView.swift | 14 +- .../AccountInspectorViewController.swift | 50 +- .../FeedInspectorViewController.swift | 73 ++- iOS/Inspector/InspectorIconHeaderView.swift | 6 +- iOS/IntentsExtension/IntentHandler.swift | 4 +- iOS/KeyboardManager.swift | 48 +- iOS/MainFeed/Cell/MainFeedRowIdentifier.swift | 6 +- iOS/MainFeed/Cell/MainFeedTableViewCell.swift | 40 +- .../Cell/MainFeedTableViewCellLayout.swift | 24 +- .../Cell/MainFeedTableViewSectionHeader.swift | 42 +- ...MainFeedTableViewSectionHeaderLayout.swift | 20 +- .../Cell/MainFeedUnreadCountView.swift | 27 +- .../MainFeedViewController+Drag.swift | 10 +- .../MainFeedViewController+Drop.swift | 37 +- iOS/MainFeed/MainFeedViewController.swift | 60 +- iOS/MainFeed/RefreshProgressView.swift | 14 +- .../MainTimelineAccessibilityCellLayout.swift | 20 +- .../Cell/MainTimelineCellData.swift | 15 +- .../Cell/MainTimelineCellLayout.swift | 39 +- .../Cell/MainTimelineDefaultCellLayout.swift | 18 +- .../Cell/MainTimelineTableViewCell.swift | 70 +-- .../Cell/MainUnreadIndicatorView.swift | 2 +- .../Cell/MultilineUILabelSizer.swift | 23 +- .../Cell/SingleLineUILabelSizer.swift | 8 +- iOS/MainTimeline/MainTimelineDataSource.swift | 5 +- iOS/MainTimeline/MainTimelineTitleView.swift | 7 +- .../MainTimelineUnreadCountView.swift | 8 +- .../MarkAsReadAlertController.swift | 26 +- iOS/MainTimeline/TimelineViewController.swift | 284 +++++---- iOS/RootSplitViewController.swift | 54 +- iOS/SceneCoordinator.swift | 577 +++++++++--------- iOS/SceneDelegate.swift | 89 ++- iOS/Settings/AboutViewController.swift | 14 +- iOS/Settings/AddAccountViewController.swift | 46 +- iOS/Settings/ArticleThemeImporter.swift | 28 +- .../ArticleThemesTableViewController.swift | 36 +- iOS/Settings/SettingsComboTableViewCell.swift | 4 +- iOS/Settings/SettingsViewController.swift | 99 ++- .../TimelineCustomizerViewController.swift | 24 +- .../TimelinePreviewTableViewController.swift | 16 +- .../ShareFolderPickerCell.swift | 2 +- .../ShareFolderPickerController.swift | 18 +- iOS/ShareExtension/ShareViewController.swift | 50 +- iOS/TitleActivityItemSource.swift | 2 +- iOS/UIKit Extensions/Animations.swift | 8 +- iOS/UIKit Extensions/Array-Extensions.swift | 6 +- iOS/UIKit Extensions/Bundle-Extensions.swift | 8 +- .../CroppingPreviewParameters.swift | 4 +- iOS/UIKit Extensions/ImageHeaderView.swift | 8 +- iOS/UIKit Extensions/InteractiveLabel.swift | 3 +- .../InteractiveNavigationController.swift | 16 +- .../ModalNavigationController.swift | 4 +- iOS/UIKit Extensions/NonIntrinsicLabel.swift | 2 +- .../PoppableGestureRecognizerDelegate.swift | 6 +- iOS/UIKit Extensions/String-Extensions.swift | 6 +- iOS/UIKit Extensions/TickMarkSlider.swift | 26 +- .../UIActivityViewController-Extensions.swift | 2 +- .../UIBarButtonItem-Extensions.swift | 6 +- iOS/UIKit Extensions/UIFont-Extensions.swift | 12 +- .../UIPageViewController-Extensions.swift | 4 +- .../UIStoryboard-Extensions.swift | 22 +- .../UITableView-Extensions.swift | 8 +- .../UIViewController-Extensions.swift | 16 +- iOS/UIKit Extensions/VibrantButton.swift | 6 +- iOS/UIKit Extensions/VibrantLabel.swift | 6 +- .../VibrantTableViewCell.swift | 32 +- 92 files changed, 1651 insertions(+), 1694 deletions(-) diff --git a/iOS/Account/CloudKitAccountViewController.swift b/iOS/Account/CloudKitAccountViewController.swift index e6060a4c5..9b560fb94 100644 --- a/iOS/Account/CloudKitAccountViewController.swift +++ b/iOS/Account/CloudKitAccountViewController.swift @@ -12,7 +12,7 @@ import Account enum CloudKitAccountViewControllerError: LocalizedError { case iCloudDriveMissing - + var errorDescription: String? { return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Unable to add iCloud Account.") } @@ -22,14 +22,14 @@ class CloudKitAccountViewController: UITableViewController { weak var delegate: AddAccountDismissDelegate? @IBOutlet weak var footerLabel: UILabel! - + override func viewDidLoad() { super.viewDidLoad() setupFooter() - + tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") } - + private func setupFooter() { footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud") } @@ -38,22 +38,22 @@ class CloudKitAccountViewController: UITableViewController { dismiss(animated: true, completion: nil) delegate?.dismiss() } - + @IBAction func add(_ sender: Any) { guard FileManager.default.ubiquityIdentityToken != nil else { presentError(CloudKitAccountViewControllerError.iCloudDriveMissing) return } - - let _ = AccountManager.shared.createAccount(type: .cloudKit) + + _ = AccountManager.shared.createAccount(type: .cloudKit) dismiss(animated: true, completion: nil) delegate?.dismiss() } - + 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? { if section == 0 { let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView diff --git a/iOS/Account/FeedbinAccountViewController.swift b/iOS/Account/FeedbinAccountViewController.swift index a988c748f..28a3942bc 100644 --- a/iOS/Account/FeedbinAccountViewController.swift +++ b/iOS/Account/FeedbinAccountViewController.swift @@ -32,7 +32,7 @@ class FeedbinAccountViewController: UITableViewController { activityIndicator.isHidden = true emailTextField.delegate = self passwordTextField.delegate = self - + if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) actionButton.isEnabled = true @@ -47,7 +47,7 @@ class FeedbinAccountViewController: UITableViewController { tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") } - + private func setupFooter() { footerLabel.text = NSLocalizedString("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a Feedbin account?", comment: "Feedbin") } @@ -55,7 +55,7 @@ class FeedbinAccountViewController: UITableViewController { 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? { if section == 0 { let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView @@ -69,7 +69,7 @@ class FeedbinAccountViewController: UITableViewController { @IBAction func cancel(_ sender: Any) { dismiss(animated: true, completion: nil) } - + @IBAction func showHidePassword(_ sender: Any) { if passwordTextField.isSecureTextEntry { passwordTextField.isSecureTextEntry = false @@ -79,21 +79,21 @@ class FeedbinAccountViewController: UITableViewController { showHideButton.setTitle("Show", for: .normal) } } - + @IBAction func action(_ sender: Any) { guard let email = emailTextField.text, let password = passwordTextField.text else { showError(NSLocalizedString("Username & password required.", comment: "Credentials Error")) return } - + // When you fill in the email address via auto-complete it adds extra whitespace let trimmedEmail = email.trimmingCharacters(in: .whitespaces) - + guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else { showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error")) return } - + resignFirstResponder() toggleActivityIndicatorAnimation(visible: true) setNavigationEnabled(to: false) @@ -102,22 +102,22 @@ class FeedbinAccountViewController: UITableViewController { Account.validateCredentials(type: .feedbin, credentials: credentials) { result in self.toggleActivityIndicatorAnimation(visible: false) self.setNavigationEnabled(to: true) - + switch result { case .success(let credentials): if let credentials = credentials { if self.account == nil { self.account = AccountManager.shared.createAccount(type: .feedbin) } - + do { - + do { try self.account?.removeCredentials(type: .basic) } catch {} try self.account?.storeCredentials(credentials) - - self.account?.refreshAll() { result in + + self.account?.refreshAll { result in switch result { case .success: break @@ -125,7 +125,7 @@ class FeedbinAccountViewController: UITableViewController { self.presentError(error) } } - + self.dismiss(animated: true, completion: nil) self.delegate?.dismiss() } catch { @@ -137,32 +137,31 @@ class FeedbinAccountViewController: UITableViewController { case .failure: self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")) } - + } } - + @IBAction func signUpWithProvider(_ sender: Any) { let url = URL(string: "https://feedbin.com/signup")! let safari = SFSafariViewController(url: url) safari.modalPresentationStyle = .currentContext self.present(safari, animated: true, completion: nil) } - - + @objc func textDidChange(_ note: Notification) { - actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false) + actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false) } - + private func showError(_ message: String) { presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message) } - - private func setNavigationEnabled(to value:Bool){ + + private func setNavigationEnabled(to value: Bool) { cancelBarButtonItem.isEnabled = value actionButton.isEnabled = value } - - private func toggleActivityIndicatorAnimation(visible value: Bool){ + + private func toggleActivityIndicatorAnimation(visible value: Bool) { activityIndicator.isHidden = !value if value { activityIndicator.startAnimating() @@ -170,11 +169,11 @@ class FeedbinAccountViewController: UITableViewController { activityIndicator.stopAnimating() } } - + } extension FeedbinAccountViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { if textField == emailTextField { passwordTextField.becomeFirstResponder() @@ -184,5 +183,5 @@ extension FeedbinAccountViewController: UITextFieldDelegate { } return true } - + } diff --git a/iOS/Account/LocalAccountViewController.swift b/iOS/Account/LocalAccountViewController.swift index 31c415d8b..46b8e018d 100644 --- a/iOS/Account/LocalAccountViewController.swift +++ b/iOS/Account/LocalAccountViewController.swift @@ -13,7 +13,7 @@ class LocalAccountViewController: UITableViewController { @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var footerLabel: UILabel! - + weak var delegate: AddAccountDismissDelegate? override func viewDidLoad() { @@ -23,7 +23,7 @@ class LocalAccountViewController: UITableViewController { nameTextField.delegate = self tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") } - + private func setupFooter() { footerLabel.text = NSLocalizedString("Local accounts do not sync your feeds across devices.", comment: "Local") } @@ -31,18 +31,18 @@ class LocalAccountViewController: UITableViewController { @IBAction func cancel(_ sender: Any) { dismiss(animated: true, completion: nil) } - + @IBAction func add(_ sender: Any) { let account = AccountManager.shared.createAccount(type: .onMyMac) account.name = nameTextField.text dismiss(animated: true, completion: nil) delegate?.dismiss() } - + 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? { if section == 0 { let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView @@ -52,14 +52,14 @@ class LocalAccountViewController: UITableViewController { return super.tableView(tableView, viewForHeaderInSection: section) } } - + } extension LocalAccountViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } - + } diff --git a/iOS/Account/NewsBlurAccountViewController.swift b/iOS/Account/NewsBlurAccountViewController.swift index d40e84fd6..803197eec 100644 --- a/iOS/Account/NewsBlurAccountViewController.swift +++ b/iOS/Account/NewsBlurAccountViewController.swift @@ -46,7 +46,7 @@ class NewsBlurAccountViewController: UITableViewController { tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") } - + private func setupFooter() { footerLabel.text = NSLocalizedString("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a NewsBlur account?", comment: "NewsBlur") } @@ -93,7 +93,7 @@ class NewsBlurAccountViewController: UITableViewController { showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error")) return } - + let password = passwordTextField.text ?? "" startAnimatingActivityIndicator() @@ -122,7 +122,7 @@ class NewsBlurAccountViewController: UITableViewController { try self.account?.storeCredentials(basicCredentials) try self.account?.storeCredentials(sessionCredentials) - self.account?.refreshAll() { result in + self.account?.refreshAll { result in switch result { case .success: break @@ -145,7 +145,7 @@ class NewsBlurAccountViewController: UITableViewController { } } - + @IBAction func signUpWithProvider(_ sender: Any) { let url = URL(string: "https://newsblur.com")! let safari = SFSafariViewController(url: url) diff --git a/iOS/Account/ReaderAPIAccountViewController.swift b/iOS/Account/ReaderAPIAccountViewController.swift index c9d081069..ce16b6607 100644 --- a/iOS/Account/ReaderAPIAccountViewController.swift +++ b/iOS/Account/ReaderAPIAccountViewController.swift @@ -27,7 +27,7 @@ class ReaderAPIAccountViewController: UITableViewController { weak var account: Account? var accountType: AccountType? weak var delegate: AddAccountDismissDelegate? - + override func viewDidLoad() { super.viewDidLoad() setupFooter() @@ -35,7 +35,7 @@ class ReaderAPIAccountViewController: UITableViewController { activityIndicator.isHidden = true usernameTextField.delegate = self passwordTextField.delegate = self - + if let unwrappedAccount = account, let credentials = try? retrieveCredentialsForAccount(for: unwrappedAccount) { actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) @@ -45,7 +45,7 @@ class ReaderAPIAccountViewController: UITableViewController { } else { actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) } - + if let unwrappedAccountType = accountType { switch unwrappedAccountType { case .freshRSS: @@ -61,14 +61,14 @@ class ReaderAPIAccountViewController: UITableViewController { title = "" } } - + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - + } - + private func setupFooter() { switch accountType { case .bazQux: @@ -87,11 +87,11 @@ class ReaderAPIAccountViewController: UITableViewController { return } } - + 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? { if section == 0 { let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView @@ -101,7 +101,7 @@ class ReaderAPIAccountViewController: UITableViewController { return super.tableView(tableView, viewForHeaderInSection: section) } } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { case 0: @@ -115,8 +115,7 @@ class ReaderAPIAccountViewController: UITableViewController { return 1 } } - - + @IBAction func cancel(_ sender: Any) { dismiss(animated: true, completion: nil) } @@ -130,19 +129,19 @@ class ReaderAPIAccountViewController: UITableViewController { showHideButton.setTitle("Show", for: .normal) } } - + @IBAction func action(_ sender: Any) { guard validateDataEntry(), let type = accountType else { return } - + let username = usernameTextField.text! let password = passwordTextField.text! let url = apiURL()! - + // When you fill in the email address via auto-complete it adds extra whitespace let trimmedUsername = username.trimmingCharacters(in: .whitespaces) - + guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else { showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error")) return @@ -167,15 +166,15 @@ class ReaderAPIAccountViewController: UITableViewController { do { self.account?.endpointURL = url - + try? self.account?.removeCredentials(type: .readerBasic) try? self.account?.removeCredentials(type: .readerAPIKey) try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(validatedCredentials) self.dismiss(animated: true, completion: nil) - - self.account?.refreshAll() { result in + + self.account?.refreshAll { result in switch result { case .success: break @@ -183,7 +182,7 @@ class ReaderAPIAccountViewController: UITableViewController { self.showError(NSLocalizedString(error.localizedDescription, comment: "Account Refresh Error")) } } - + self.delegate?.dismiss() } catch { self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) @@ -197,7 +196,7 @@ class ReaderAPIAccountViewController: UITableViewController { } } - + private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? { switch accountType { case .bazQux, .inoreader, .theOldReader, .freshRSS: @@ -206,7 +205,7 @@ class ReaderAPIAccountViewController: UITableViewController { return nil } } - + private func headerViewImage() -> UIImage? { if let accountType = accountType { switch accountType { @@ -224,7 +223,7 @@ class ReaderAPIAccountViewController: UITableViewController { } return nil } - + private func validateDataEntry() -> Bool { switch accountType { case .freshRSS: @@ -244,7 +243,7 @@ class ReaderAPIAccountViewController: UITableViewController { } return true } - + @IBAction func signUpWithProvider(_ sender: Any) { var url: URL! switch accountType { @@ -263,7 +262,7 @@ class ReaderAPIAccountViewController: UITableViewController { safari.modalPresentationStyle = .currentContext self.present(safari, animated: true, completion: nil) } - + private func apiURL() -> URL? { switch accountType { case .freshRSS: @@ -278,9 +277,7 @@ class ReaderAPIAccountViewController: UITableViewController { return nil } } - - - + @objc func textDidChange(_ note: Notification) { actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) } diff --git a/iOS/Add/AddComboTableViewCell.swift b/iOS/Add/AddComboTableViewCell.swift index 4e7c30155..e5e710eae 100644 --- a/iOS/Add/AddComboTableViewCell.swift +++ b/iOS/Add/AddComboTableViewCell.swift @@ -12,10 +12,10 @@ class AddComboTableViewCell: VibrantTableViewCell { @IBOutlet weak var icon: UIImageView! @IBOutlet weak var label: UILabel! - + override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) - + let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor if animated { UIView.animate(withDuration: Self.duration) { @@ -26,5 +26,5 @@ class AddComboTableViewCell: VibrantTableViewCell { } updateLabelVibrancy(label, color: labelColor, animated: animated) } - + } diff --git a/iOS/Add/AddFeedFolderViewController.swift b/iOS/Add/AddFeedFolderViewController.swift index 5c75bbb94..8240ace44 100644 --- a/iOS/Add/AddFeedFolderViewController.swift +++ b/iOS/Add/AddFeedFolderViewController.swift @@ -15,15 +15,15 @@ protocol AddFeedFolderViewControllerDelegate { } class AddFeedFolderViewController: UITableViewController { - + var delegate: AddFeedFolderViewControllerDelegate? var initialContainer: Container? - + var containers = [Container]() override func viewDidLoad() { super.viewDidLoad() - + let sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts for account in sortedActiveAccounts { @@ -53,15 +53,15 @@ class AddFeedFolderViewController: UITableViewController { return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! AddComboTableViewCell } }() - + if let smallIconProvider = container as? SmallIconProvider { cell.icon?.image = smallIconProvider.smallIcon?.image } - + if let displayNameProvider = container as? DisplayNameProvider { cell.label?.text = displayNameProvider.nameForDisplay } - + if let compContainer = initialContainer, container === compContainer { cell.accessoryType = .checkmark } else { @@ -73,7 +73,7 @@ class AddFeedFolderViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let container = containers[indexPath.row] - + if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) { tableView.selectRow(at: nil, animated: false, scrollPosition: .none) } else { @@ -83,19 +83,19 @@ class AddFeedFolderViewController: UITableViewController { dismiss() } } - + // MARK: Actions - + @IBAction func cancel(_ sender: Any) { dismiss() } - + } private extension AddFeedFolderViewController { - + func dismiss() { dismiss(animated: true) } - + } diff --git a/iOS/Add/AddFeedSelectFolderTableViewCell.swift b/iOS/Add/AddFeedSelectFolderTableViewCell.swift index 2ce871397..ea91f7405 100644 --- a/iOS/Add/AddFeedSelectFolderTableViewCell.swift +++ b/iOS/Add/AddFeedSelectFolderTableViewCell.swift @@ -9,14 +9,14 @@ import UIKit class AddFeedSelectFolderTableViewCell: VibrantTableViewCell { - + @IBOutlet weak var folderLabel: UILabel! @IBOutlet weak var detailLabel: UILabel! - + override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) updateLabelVibrancy(folderLabel, color: labelColor, animated: animated) updateLabelVibrancy(detailLabel, color: labelColor, animated: animated) } - + } diff --git a/iOS/Add/AddFeedViewController.swift b/iOS/Add/AddFeedViewController.swift index 09a474831..c4cc28b41 100644 --- a/iOS/Add/AddFeedViewController.swift +++ b/iOS/Add/AddFeedViewController.swift @@ -19,9 +19,9 @@ class AddFeedViewController: UITableViewController { @IBOutlet weak var urlTextField: UITextField! @IBOutlet weak var urlTextFieldToSuperViewConstraint: NSLayoutConstraint! @IBOutlet weak var nameTextField: UITextField! - + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) - + private var folderLabel = "" private var userCancelled = false @@ -29,88 +29,88 @@ class AddFeedViewController: UITableViewController { var initialFeedName: String? var container: Container? - + override func viewDidLoad() { super.viewDidLoad() activityIndicator.isHidden = true activityIndicator.color = .label - + if initialFeed == nil, let urlString = UIPasteboard.general.string { if urlString.mayBeURL { initialFeed = urlString.normalizedURL } } - + urlTextField.autocorrectionType = .no urlTextField.autocapitalizationType = .none urlTextField.text = initialFeed urlTextField.delegate = self - + if initialFeed != nil { addButton.isEnabled = true } - + nameTextField.text = initialFeedName nameTextField.delegate = self - + if let defaultContainer = AddFeedDefaultContainer.defaultContainer { container = defaultContainer } else { addButton.isEnabled = false } - + updateFolderLabel() - + tableView.register(UINib(nibName: "AddFeedSelectFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "AddFeedSelectFolderTableViewCell") NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: urlTextField) - + if initialFeed == nil { urlTextField.becomeFirstResponder() } } - + @IBAction func cancel(_ sender: Any) { userCancelled = true dismiss(animated: true) } - + @IBAction func add(_ sender: Any) { let urlString = urlTextField.text ?? "" let normalizedURLString = urlString.normalizedURL - + guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else { return } - + guard let container = container else { return } - + var account: Account? if let containerAccount = container as? Account { account = containerAccount } else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account { account = containerAccount } - + if account!.hasFeed(withURL: url.absoluteString) { presentError(AccountError.createErrorAlreadySubscribed) return } - + addButton.isEnabled = false activityIndicator.isHidden = false activityIndicator.startAnimating() - + let feedName = (nameTextField.text?.isEmpty ?? true) ? nil : nameTextField.text - + BatchUpdate.shared.start() - + account!.createFeed(url: url.absoluteString, name: feedName, container: container, validateFeed: true) { result in BatchUpdate.shared.end() - + switch result { case .success(let feed): self.dismiss(animated: true) @@ -125,11 +125,11 @@ class AddFeedViewController: UITableViewController { } } - + @objc func textDidChange(_ note: Notification) { updateUI() } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.row == 2 { let cell = tableView.dequeueReusableCell(withIdentifier: "AddFeedSelectFolderTableViewCell", for: indexPath) as? AddFeedSelectFolderTableViewCell @@ -139,7 +139,7 @@ class AddFeedViewController: UITableViewController { return super.tableView(tableView, cellForRowAt: indexPath) } } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row == 2 { let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedFolderNavViewController") as! UINavigationController @@ -150,7 +150,7 @@ class AddFeedViewController: UITableViewController { present(navController, animated: true) } } - + } // MARK: AddFeedFolderViewControllerDelegate @@ -166,22 +166,22 @@ extension AddFeedViewController: AddFeedFolderViewControllerDelegate { // MARK: UITextFieldDelegate extension AddFeedViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } - + } // MARK: Private private extension AddFeedViewController { - + func updateUI() { addButton.isEnabled = (urlTextField.text?.mayBeURL ?? false) } - + func updateFolderLabel() { if let containerName = (container as? DisplayNameProvider)?.nameForDisplay { if container is Folder { diff --git a/iOS/Add/AddFolderViewController.swift b/iOS/Add/AddFolderViewController.swift index 528e443c9..23bcc9aed 100644 --- a/iOS/Add/AddFolderViewController.swift +++ b/iOS/Add/AddFolderViewController.swift @@ -16,13 +16,13 @@ class AddFolderViewController: UITableViewController { @IBOutlet private weak var nameTextField: UITextField! @IBOutlet private weak var accountLabel: UILabel! @IBOutlet private weak var accountPickerView: UIPickerView! - + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) - + private var shouldDisplayPicker: Bool { return accounts.count > 1 } - + private var accounts: [Account]! { didSet { if let predefinedAccount = accounts.first(where: { $0.accountID == AppDefaults.shared.addFolderAccountID }) { @@ -39,33 +39,33 @@ class AddFolderViewController: UITableViewController { accountLabel.text = selectedAccount.flatMap { ($0 as DisplayNameProvider).nameForDisplay } } } - + override func viewDidLoad() { super.viewDidLoad() - + accounts = AccountManager.shared .sortedActiveAccounts .filter { !$0.behaviors.contains(.disallowFolderManagement) } - + nameTextField.delegate = self - + if shouldDisplayPicker { accountPickerView.dataSource = self accountPickerView.delegate = self - + if let index = accounts.firstIndex(of: selectedAccount) { accountPickerView.selectRow(index, inComponent: 0, animated: false) } - + } else { accountPickerView.isHidden = true } - + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: nameTextField) - + nameTextField.becomeFirstResponder() } - + private func didSelect(_ account: Account) { AppDefaults.shared.addFolderAccountID = account.accountID selectedAccount = account @@ -74,7 +74,7 @@ class AddFolderViewController: UITableViewController { @IBAction func cancel(_ sender: Any) { dismiss(animated: true) } - + @IBAction func add(_ sender: Any) { guard let folderName = nameTextField.text else { return @@ -93,42 +93,42 @@ class AddFolderViewController: UITableViewController { @objc func textDidChange(_ note: Notification) { addButton.isEnabled = !(nameTextField.text?.isEmpty ?? false) } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) if section == 1 && !shouldDisplayPicker { return defaultNumberOfRows - 1 } - - return defaultNumberOfRows + + return defaultNumberOfRows } } extension AddFolderViewController: UIPickerViewDataSource, UIPickerViewDelegate { - - func numberOfComponents(in pickerView: UIPickerView) ->Int { + + func numberOfComponents(in pickerView: UIPickerView) -> Int { return 1 } - + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { return accounts.count } - + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { return (accounts[row] as DisplayNameProvider).nameForDisplay } - + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { didSelect(accounts[row]) } - + } extension AddFolderViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } - + } diff --git a/iOS/Add/SelectComboTableViewCell.swift b/iOS/Add/SelectComboTableViewCell.swift index d194a871a..ab69c72fd 100644 --- a/iOS/Add/SelectComboTableViewCell.swift +++ b/iOS/Add/SelectComboTableViewCell.swift @@ -12,10 +12,10 @@ class SelectComboTableViewCell: VibrantTableViewCell { @IBOutlet weak var icon: UIImageView! @IBOutlet weak var label: UILabel! - + override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) - + let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label if animated { UIView.animate(withDuration: Self.duration) { @@ -24,8 +24,8 @@ class SelectComboTableViewCell: VibrantTableViewCell { } else { self.icon.tintColor = iconTintColor } - + updateLabelVibrancy(label, color: labelColor, animated: animated) } - + } diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index ae409e96d..16e3d2d31 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -10,7 +10,7 @@ import RSCore import Account struct AppAssets { - + static var accountBazQuxImage: UIImage = { return UIImage(named: "accountBazQux")! }() @@ -90,43 +90,43 @@ struct AppAssets { static var circleClosedImage: UIImage = { return UIImage(systemName: "largecircle.fill.circle")! }() - + static var circleOpenImage: UIImage = { return UIImage(systemName: "circle")! }() - + static var disclosureImage: UIImage = { return UIImage(named: "disclosure")! }() - + static var copyImage: UIImage = { return UIImage(systemName: "doc.on.doc")! }() - + static var deactivateImage: UIImage = { UIImage(systemName: "minus.circle")! }() - + static var editImage: UIImage = { UIImage(systemName: "square.and.pencil")! }() - + static var faviconTemplateImage: RSImage = { return RSImage(named: "faviconTemplateImage")! }() - + static var filterInactiveImage: UIImage = { UIImage(systemName: "line.horizontal.3.decrease.circle")! }() - + static var filterActiveImage: UIImage = { UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! }() - + static var folderOutlinePlus: UIImage = { UIImage(systemName: "folder.badge.plus")! }() - + static var fullScreenBackgroundColor: UIColor = { return UIColor(named: "fullScreenBackgroundColor")! }() @@ -134,19 +134,19 @@ struct AppAssets { static var infoImage: UIImage = { UIImage(systemName: "info.circle")! }() - + static var markAllAsReadImage: UIImage = { return UIImage(named: "markAllAsRead")! }() - + static var markBelowAsReadImage: UIImage = { return UIImage(systemName: "arrowtriangle.down.circle")! }() - + static var markAboveAsReadImage: UIImage = { return UIImage(systemName: "arrowtriangle.up.circle")! }() - + static var folderImage: IconImage = { return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) }() @@ -154,67 +154,67 @@ struct AppAssets { static var moreImage: UIImage = { return UIImage(systemName: "ellipsis.circle")! }() - + static var nextArticleImage: UIImage = { return UIImage(systemName: "chevron.down")! }() - + static var nextUnreadArticleImage: UIImage = { return UIImage(systemName: "chevron.down.circle")! }() - + static var plus: UIImage = { UIImage(systemName: "plus")! }() - + static var prevArticleImage: UIImage = { return UIImage(systemName: "chevron.up")! }() - + static var openInSidebarImage: UIImage = { return UIImage(systemName: "arrow.turn.down.left")! }() - + static var primaryAccentColor: UIColor { return UIColor(named: "primaryAccentColor")! } - + static var safariImage: UIImage = { return UIImage(systemName: "safari")! }() - + static var searchFeedImage: IconImage = { return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) }() - + static var secondaryAccentColor: UIColor { return UIColor(named: "secondaryAccentColor")! } - + static var sectionHeaderColor: UIColor = { return UIColor(named: "sectionHeaderColor")! }() - + static var shareImage: UIImage = { return UIImage(systemName: "square.and.arrow.up")! }() - + static var smartFeedImage: UIImage = { return UIImage(systemName: "gear")! }() - + static var starColor: UIColor = { return UIColor(named: "starColor")! }() - + static var starClosedImage: UIImage = { return UIImage(systemName: "star.fill")! }() - + static var starOpenImage: UIImage = { return UIImage(systemName: "star")! }() - + static var starredFeedImage: IconImage { let image = UIImage(systemName: "star.fill")! return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.starColor.cgColor) @@ -223,12 +223,12 @@ struct AppAssets { static var tickMarkColor: UIColor = { return UIColor(named: "tickMarkColor")! }() - + static var timelineStarImage: UIImage = { let image = UIImage(systemName: "star.fill")! return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal) }() - + static var todayFeedImage: IconImage { let image = UIImage(systemName: "sun.max.fill")! return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor) @@ -237,12 +237,12 @@ struct AppAssets { static var trashImage: UIImage = { return UIImage(systemName: "trash")! }() - + static var unreadFeedImage: IconImage { let image = UIImage(systemName: "largecircle.fill.circle")! return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) } - + static var vibrantTextColor: UIColor = { return UIColor(named: "vibrantTextColor")! }() @@ -251,7 +251,6 @@ struct AppAssets { return UIColor(named: "controlBackgroundColor")! }() - static func image(for accountType: AccountType) -> UIImage? { switch accountType { case .onMyMac: @@ -278,5 +277,5 @@ struct AppAssets { return AppAssets.accountTheOldReaderImage } } - + } diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 5f676b8fc..66dd18809 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -23,22 +23,22 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { return NSLocalizedString("Dark", comment: "Dark") } } - + } final class AppDefaults { static let defaultThemeName = "Default" - + static let shared = AppDefaults() private init() {} - + static var store: UserDefaults = { let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)" return UserDefaults.init(suiteName: suiteName)! }() - + struct Key { static let userInterfaceColorPalette = "userInterfaceColorPalette" static let lastImageCacheFlushDate = "lastImageCacheFlushDate" @@ -74,7 +74,7 @@ final class AppDefaults { firstRunDate = Date() return true }() - + static var userInterfaceColorPalette: UserInterfaceColorPalette { get { if let result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) { @@ -95,7 +95,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.addFeedAccountID, newValue) } } - + var addFeedFolderName: String? { get { return AppDefaults.string(for: Key.addFeedFolderName) @@ -104,7 +104,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.addFeedFolderName, newValue) } } - + var addFolderAccountID: String? { get { return AppDefaults.string(for: Key.addFolderAccountID) @@ -113,7 +113,7 @@ final class AppDefaults { AppDefaults.setString(for: Key.addFolderAccountID, newValue) } } - + var useSystemBrowser: Bool { get { return UserDefaults.standard.bool(forKey: Key.useSystemBrowser) @@ -122,7 +122,7 @@ final class AppDefaults { UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser) } } - + var lastImageCacheFlushDate: Date? { get { return AppDefaults.date(for: Key.lastImageCacheFlushDate) @@ -189,7 +189,7 @@ final class AppDefaults { AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue) } } - + var lastRefresh: Date? { get { return AppDefaults.date(for: Key.lastRefresh) @@ -198,7 +198,7 @@ final class AppDefaults { AppDefaults.setDate(for: Key.lastRefresh, newValue) } } - + var timelineNumberOfLines: Int { get { return AppDefaults.int(for: Key.timelineNumberOfLines) @@ -207,7 +207,7 @@ final class AppDefaults { AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue) } } - + var timelineIconSize: IconSize { get { let rawValue = AppDefaults.store.integer(forKey: Key.timelineIconDimension) @@ -217,7 +217,7 @@ final class AppDefaults { AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension) } } - + var currentThemeName: String? { get { return AppDefaults.string(for: Key.currentThemeName) @@ -237,7 +237,7 @@ final class AppDefaults { } static func registerDefaults() { - let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, + let defaults: [String: Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, Key.timelineGroupByFeed: false, Key.refreshClearsReadArticles: false, Key.timelineNumberOfLines: 2, @@ -266,7 +266,7 @@ private extension AppDefaults { static func string(for key: String) -> String? { return UserDefaults.standard.string(forKey: key) } - + static func setString(for key: String, _ value: String?) { UserDefaults.standard.set(value, forKey: key) } @@ -282,11 +282,11 @@ private extension AppDefaults { static func int(for key: String) -> Int { return AppDefaults.store.integer(forKey: key) } - + static func setInt(for key: String, _ x: Int) { AppDefaults.store.set(x, forKey: key) } - + static func date(for key: String) -> Date? { return AppDefaults.store.object(forKey: key) as? Date } @@ -295,7 +295,7 @@ private extension AppDefaults { AppDefaults.store.set(date, forKey: key) } - static func sortDirection(for key:String) -> ComparisonResult { + static func sortDirection(for key: String) -> ComparisonResult { let rawInt = int(for: key) if rawInt == ComparisonResult.orderedAscending.rawValue { return .orderedAscending @@ -306,10 +306,9 @@ private extension AppDefaults { static func setSortDirection(for key: String, _ value: ComparisonResult) { if value == .orderedAscending { setInt(for: key, ComparisonResult.orderedAscending.rawValue) - } - else { + } else { setInt(for: key, ComparisonResult.orderedDescending.rawValue) } } - + } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 70f7b9916..5f0115b52 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -19,14 +19,14 @@ var appDelegate: AppDelegate! @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider { - + private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler") - + private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - + var syncTimer: ArticleStatusSyncTimer? - + var shuttingDown = false { didSet { if shuttingDown { @@ -35,7 +35,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } } - + private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application") var userNotificationManager: UserNotificationManager! @@ -52,10 +52,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } } - + var isSyncArticleStatusRunning = false var isWaitingForSyncTasks = false - + override init() { super.init() appDelegate = self @@ -64,15 +64,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath) - + let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath) - + NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil) } - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { AppDefaults.registerDefaults() @@ -80,22 +80,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if isFirstRun { logger.info("Is first run.") } - + if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { let localAccount = AccountManager.shared.defaultAccount DefaultFeedsImporter.importDefaultFeeds(account: localAccount) } - + registerBackgroundTasks() CacheCleaner.purgeIfNecessary() initializeDownloaders() initializeHomeScreenQuickActions() - + DispatchQueue.main.async { self.unreadCount = AccountManager.shared.unreadCount } - - UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in + + UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() @@ -108,7 +108,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD extensionContainersFile = ExtensionContainersFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() - + widgetDataEncoder = WidgetDataEncoder() syncTimer = ArticleStatusSyncTimer() @@ -116,12 +116,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD #if DEBUG syncTimer!.update() #endif - + return true - + } - - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { DispatchQueue.main.async { self.resumeDatabaseProcessingIfNecessary() AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { @@ -130,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } } - + func applicationWillTerminate(_ application: UIApplication) { shuttingDown = true } @@ -138,35 +138,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func applicationDidEnterBackground(_ application: UIApplication) { IconImageCache.shared.emptyCache() } - + // MARK: Notifications - + @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount } } - + @objc func accountRefreshDidFinish(_ note: Notification) { AppDefaults.shared.lastRefresh = Date() } - + // MARK: - API - - func manualRefresh(errorHandler: @escaping (Error) -> ()) { - UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach { + + func manualRefresh(errorHandler: @escaping (Error) -> Void) { + UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate }).forEach { $0.cleanUp(conditional: true) } AccountManager.shared.refreshAll(errorHandler: errorHandler) } - + func resumeDatabaseProcessingIfNecessary() { if AccountManager.shared.isSuspended { AccountManager.shared.resumeAll() logger.info("Application processing resumed.") } } - + func prepareAccountsForBackground() { extensionFeedAddRequestFile.suspend() syncTimer?.invalidate() @@ -190,16 +190,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) } } - + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.list, .banner, .badge, .sound]) } - + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { defer { completionHandler() } - + let userInfo = response.notification.request.content.userInfo - + switch response.actionIdentifier { case "MARK_AS_READ": handleMarkAsRead(userInfo: userInfo) @@ -213,15 +213,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD }) } } - + } - + } // MARK: App Initialization private extension AppDelegate { - + private func initializeDownloaders() { let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! let faviconsFolderURL = tempDir.appendingPathComponent("Favicons") @@ -239,7 +239,7 @@ private extension AppDelegate { let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread") let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle") let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil) - + let searchTitle = NSLocalizedString("Search", comment: "Search") let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass") let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil) @@ -250,38 +250,38 @@ private extension AppDelegate { UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem] } - + } // MARK: Go To Background private extension AppDelegate { - + func waitForSyncTasksToFinish() { guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return } - + isWaitingForSyncTasks = true - + self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in guard let self = self else { return } self.completeProcessing(true) logger.info("Accounts wait for progress terminated for running too long.") } - + DispatchQueue.main.async { [weak self] in - self?.waitToComplete() { [weak self] suspend in + self?.waitToComplete { [weak self] suspend in self?.completeProcessing(suspend) } } } - + func waitToComplete(completion: @escaping (Bool) -> Void) { guard UIApplication.shared.applicationState == .background else { logger.info("App came back to foreground, no longer waiting.") completion(false) return } - + if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning { logger.info("Waiting for sync to finish…") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in @@ -292,7 +292,7 @@ private extension AppDelegate { completion(true) } } - + func completeProcessing(_ suspend: Bool) { if suspend { suspendApplication() @@ -301,33 +301,33 @@ private extension AppDelegate { self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid isWaitingForSyncTasks = false } - + func syncArticleStatus() { guard !isSyncArticleStatusRunning else { return } - + isSyncArticleStatusRunning = true - + let completeProcessing = { [unowned self] in self.isSyncArticleStatusRunning = false UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask) self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid } - + self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { completeProcessing() self.logger.info("Accounts sync processing terminated for running too long.") } - + DispatchQueue.main.async { - AccountManager.shared.syncArticleStatusAll() { + AccountManager.shared.syncArticleStatusAll { completeProcessing() } } } - + func suspendApplication() { guard UIApplication.shared.applicationState == .background else { return } - + AccountManager.shared.suspendNetworkAll() AccountManager.shared.suspendDatabaseAll() ArticleThemeDownloader.shared.cleanUp() @@ -338,10 +338,10 @@ private extension AppDelegate { sceneDelegate.suspend() } } - + logger.info("Application processing suspended.") } - + } // MARK: Background Tasks @@ -355,7 +355,7 @@ private extension AppDelegate { self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask) } } - + /// Schedules a background app refresh based on `AppDefaults.refreshInterval`. func scheduleBackgroundFeedRefresh() { let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh") @@ -371,7 +371,7 @@ private extension AppDelegate { } } } - + /// Performs background feed refresh. /// - Parameter task: `BGAppRefreshTask` /// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work. @@ -408,9 +408,9 @@ private extension AppDelegate { // Handle Notification Actions private extension AppDelegate { - + func handleMarkAsRead(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], + guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any], let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { return @@ -435,9 +435,9 @@ private extension AppDelegate { } }) } - + func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], + guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any], let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { return diff --git a/iOS/Article/ArticleExtractorButton.swift b/iOS/Article/ArticleExtractorButton.swift index cc1087888..cd2f26afd 100644 --- a/iOS/Article/ArticleExtractorButton.swift +++ b/iOS/Article/ArticleExtractorButton.swift @@ -16,9 +16,9 @@ enum ArticleExtractorButtonState { } class ArticleExtractorButton: UIButton { - + private var animatedLayer: CALayer? - + var buttonState: ArticleExtractorButtonState = .off { didSet { if buttonState != oldValue { @@ -39,7 +39,7 @@ class ArticleExtractorButton: UIButton { } } } - + override var accessibilityLabel: String? { get { switch buttonState { @@ -57,7 +57,7 @@ class ArticleExtractorButton: UIButton { super.accessibilityLabel = newValue } } - + override func layoutSubviews() { super.layoutSubviews() guard case .animated = buttonState else { @@ -66,31 +66,31 @@ class ArticleExtractorButton: UIButton { stripAnimatedSublayer() addAnimatedSublayer(to: layer) } - + private func stripAnimatedSublayer() { animatedLayer?.removeFromSuperlayer() } - + private func addAnimatedSublayer(to hostedLayer: CALayer) { let image1 = AppAssets.articleExtractorOffTinted.cgImage! let image2 = AppAssets.articleExtractorOnTinted.cgImage! let images = [image1, image2, image1] - + animatedLayer = CALayer() let imageSize = AppAssets.articleExtractorOff.size animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height) animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY) - + hostedLayer.addSublayer(animatedLayer!) - + let animation = CAKeyframeAnimation(keyPath: "contents") animation.calculationMode = CAAnimationCalculationMode.linear animation.keyTimes = [0, 0.5, 1] animation.duration = 2 animation.values = images as [Any] animation.repeatCount = HUGE - + animatedLayer!.add(animation, forKey: "contents") } - + } diff --git a/iOS/Article/ArticleSearchBar.swift b/iOS/Article/ArticleSearchBar.swift index c69cfdd4c..4c8ba37ed 100644 --- a/iOS/Article/ArticleSearchBar.swift +++ b/iOS/Article/ArticleSearchBar.swift @@ -15,15 +15,14 @@ import UIKit @objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String) } - @IBDesignable final class ArticleSearchBar: UIStackView { var searchField: UISearchTextField! var nextButton: UIButton! var prevButton: UIButton! var background: UIView! - + weak private var resultsLabel: UILabel! - + var resultsCount: UInt = 0 { didSet { updateUI() @@ -34,30 +33,30 @@ import UIKit updateUI() } } - + weak var delegate: SearchBarDelegate? - + override var keyCommands: [UIKeyCommand]? { return [UIKeyCommand(title: "Exit Find", action: #selector(donePressed(_:)), input: UIKeyCommand.inputEscape)] } - + override init(frame: CGRect) { super.init(frame: frame) commonInit() } - + required init(coder: NSCoder) { super.init(coder: coder) commonInit() } - + override func didMoveToSuperview() { super.didMoveToSuperview() layer.backgroundColor = UIColor(named: "barBackgroundColor")?.cgColor ?? UIColor.white.cgColor isOpaque = true NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: searchField) } - + private func updateUI() { if resultsCount > 0 { let format = NSLocalizedString("%d of %d", comment: "Results selection and count") @@ -65,23 +64,23 @@ import UIKit } else { resultsLabel.text = NSLocalizedString("No results", comment: "No results") } - + nextButton.isEnabled = selectedResult < resultsCount prevButton.isEnabled = resultsCount > 0 && selectedResult > 1 } - + @discardableResult override func becomeFirstResponder() -> Bool { searchField.becomeFirstResponder() } - + @discardableResult override func resignFirstResponder() -> Bool { searchField.resignFirstResponder() } - + override var isFirstResponder: Bool { searchField.isFirstResponder } - + deinit { NotificationCenter.default.removeObserver(self) } @@ -94,12 +93,12 @@ private extension ArticleSearchBar { spacing = 8 layoutMargins.left = 8 layoutMargins.right = 8 - + background = UIView(frame: bounds) background.backgroundColor = .systemGray5 background.autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(background) - + let doneButton = UIButton() doneButton.setTitle(NSLocalizedString("Done", comment: "Done"), for: .normal) doneButton.setTitleColor(UIColor.label, for: .normal) @@ -108,14 +107,14 @@ private extension ArticleSearchBar { doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside) doneButton.isEnabled = true addArrangedSubview(doneButton) - + let resultsLabel = UILabel() searchField = UISearchTextField() searchField.autocapitalizationType = .none searchField.autocorrectionType = .no searchField.returnKeyType = .search searchField.delegate = self - + resultsLabel.font = .systemFont(ofSize: UIFont.smallSystemFontSize) resultsLabel.textColor = .secondaryLabel resultsLabel.text = "" @@ -123,17 +122,17 @@ private extension ArticleSearchBar { resultsLabel.adjustsFontSizeToFitWidth = true searchField.rightView = resultsLabel searchField.rightViewMode = .always - + self.resultsLabel = resultsLabel addArrangedSubview(searchField) - + prevButton = UIButton(type: .system) prevButton.setImage(UIImage(systemName: "chevron.up"), for: .normal) prevButton.accessibilityLabel = "Previous Result" prevButton.isAccessibilityElement = true prevButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside) addArrangedSubview(prevButton) - + nextButton = UIButton(type: .system) nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal) nextButton.accessibilityLabel = "Next Result" @@ -144,25 +143,25 @@ private extension ArticleSearchBar { } private extension ArticleSearchBar { - + @objc func textDidChange(_ notification: Notification) { delegate?.searchBar?(self, textDidChange: searchField.text ?? "") - + if searchField.text?.isEmpty ?? true { searchField.rightViewMode = .never } else { searchField.rightViewMode = .always } } - + @objc func nextPressed() { delegate?.nextWasPressed?(self) } - + @objc func previousPressed() { delegate?.previousWasPressed?(self) } - + @objc func donePressed(_ _: Any? = nil) { delegate?.doneWasPressed?(self) } diff --git a/iOS/Article/ContextMenuPreviewViewController.swift b/iOS/Article/ContextMenuPreviewViewController.swift index cf321c732..d89962a31 100644 --- a/iOS/Article/ContextMenuPreviewViewController.swift +++ b/iOS/Article/ContextMenuPreviewViewController.swift @@ -16,21 +16,21 @@ final class ContextMenuPreviewViewController: UIViewController { @IBOutlet weak var blogAuthorLabel: UILabel! @IBOutlet weak var articleTitleLabel: UILabel! @IBOutlet weak var dateTimeLabel: UILabel! - + var article: Article? init(article: Article?) { self.article = article super.init(nibName: "ContextMenuPreviewViewController", bundle: nil) } - + required init?(coder: NSCoder) { super.init(coder: coder) } - + override func viewDidLoad() { super.viewDidLoad() - + blogNameLabel.text = article?.feed?.nameForDisplay ?? "" blogAuthorLabel.text = article?.byline() articleTitleLabel.text = article?.title ?? "" @@ -39,14 +39,14 @@ final class ContextMenuPreviewViewController: UIViewController { icon.iconImage = article?.iconImage() icon.translatesAutoresizingMaskIntoConstraints = false view.addSubview(icon) - + NSLayoutConstraint.activate([ icon.widthAnchor.constraint(equalToConstant: 48), icon.heightAnchor.constraint(equalToConstant: 48), icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20) ]) - + let dateFormatter = DateFormatter() dateFormatter.dateStyle = .long dateFormatter.timeStyle = .medium @@ -57,7 +57,7 @@ final class ContextMenuPreviewViewController: UIViewController { // When in landscape the context menu preview will force this controller into a tiny // view space. If it is documented anywhere what that is, I haven't found it. This // set of magic numbers is what I worked out by testing a variety of phones. - + let width: CGFloat let heightPadding: CGFloat if view.bounds.width > view.bounds.height { @@ -68,7 +68,7 @@ final class ContextMenuPreviewViewController: UIViewController { width = view.bounds.width heightPadding = 8 } - + view.setNeedsLayout() view.layoutIfNeeded() preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding) diff --git a/iOS/Article/FindInArticleActivity.swift b/iOS/Article/FindInArticleActivity.swift index 334a142fb..c55f3154c 100644 --- a/iOS/Article/FindInArticleActivity.swift +++ b/iOS/Article/FindInArticleActivity.swift @@ -12,27 +12,27 @@ class FindInArticleActivity: UIActivity { override var activityTitle: String? { NSLocalizedString("Find in Article", comment: "Find in Article") } - + override var activityType: UIActivity.ActivityType? { UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find") } - + override var activityImage: UIImage? { UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) } - + override class var activityCategory: UIActivity.Category { .action } - + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { true } - + override func prepare(withActivityItems activityItems: [Any]) { - + } - + override func perform() { NotificationCenter.default.post(Notification(name: .FindInArticle)) activityDidFinish(true) diff --git a/iOS/Article/ImageScrollView.swift b/iOS/Article/ImageScrollView.swift index 8871c2e57..b4b695d6b 100644 --- a/iOS/Article/ImageScrollView.swift +++ b/iOS/Article/ImageScrollView.swift @@ -14,63 +14,63 @@ import UIKit } open class ImageScrollView: UIScrollView { - + @objc public enum ScaleMode: Int { case aspectFill case aspectFit case widthFill case heightFill } - + @objc public enum Offset: Int { case beginning case center } - + static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2 - + @objc open var imageContentMode: ScaleMode = .widthFill @objc open var initialOffset: Offset = .beginning - - @objc public private(set) var zoomView: UIImageView? = nil - + + @objc public private(set) var zoomView: UIImageView? + @objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate? - + var imageSize: CGSize = CGSize.zero private var pointToCenterAfterResize: CGPoint = CGPoint.zero private var scaleToRestoreAfterResize: CGFloat = 1.0 var maxScaleFromMinScale: CGFloat = 3.0 - + var zoomedFrame: CGRect { return zoomView?.frame ?? CGRect.zero } - + override open var frame: CGRect { willSet { if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { prepareToResize() } } - + didSet { if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { recoverFromResizing() } } } - + override public init(frame: CGRect) { super.init(frame: frame) - + initialize() } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - + initialize() } - + private func initialize() { showsVerticalScrollIndicator = false showsHorizontalScrollIndicator = false @@ -78,135 +78,135 @@ open class ImageScrollView: UIScrollView { decelerationRate = UIScrollView.DecelerationRate.fast delegate = self } - + @objc public func adjustFrameToCenter() { - + guard let unwrappedZoomView = zoomView else { return } - + var frameToCenter = unwrappedZoomView.frame - + // center horizontally if frameToCenter.size.width < bounds.width { frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2 } else { frameToCenter.origin.x = 0 } - + // center vertically if frameToCenter.size.height < bounds.height { frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2 } else { frameToCenter.origin.y = 0 } - + unwrappedZoomView.frame = frameToCenter } - + private func prepareToResize() { let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) pointToCenterAfterResize = convert(boundsCenter, to: zoomView) - + scaleToRestoreAfterResize = zoomScale - + // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum // allowable scale when the scale is restored. if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) { scaleToRestoreAfterResize = 0 } } - + private func recoverFromResizing() { setMaxMinZoomScalesForCurrentBounds() - + // restore zoom scale, first making sure it is within the allowable range. let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize) zoomScale = min(maximumZoomScale, maxZoomScale) - + // restore center point, first making sure it is within the allowable range. - + // convert our desired center point back to our own coordinate space let boundsCenter = convert(pointToCenterAfterResize, to: zoomView) - + // calculate the content offset that would yield that center point var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0) - + // restore offset, adjusted to be within the allowable range let maxOffset = maximumContentOffset() let minOffset = minimumContentOffset() - + var realMaxOffset = min(maxOffset.x, offset.x) offset.x = max(minOffset.x, realMaxOffset) - + realMaxOffset = min(maxOffset.y, offset.y) offset.y = max(minOffset.y, realMaxOffset) - + contentOffset = offset } - + private func maximumContentOffset() -> CGPoint { - return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height) + return CGPoint(x: contentSize.width - bounds.width, y: contentSize.height - bounds.height) } - + private func minimumContentOffset() -> CGPoint { return CGPoint.zero } - + // MARK: - Set up - + open func setup() { var topSupperView = superview - + while topSupperView?.superview != nil { topSupperView = topSupperView?.superview } - + // Make sure views have already layout with precise frame topSupperView?.layoutIfNeeded() } - + // MARK: - Display image - + @objc open func display(image: UIImage) { - + if let zoomView = zoomView { zoomView.removeFromSuperview() } - + zoomView = UIImageView(image: image) zoomView!.isUserInteractionEnabled = true addSubview(zoomView!) - + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:))) tapGesture.numberOfTapsRequired = 2 zoomView!.addGestureRecognizer(tapGesture) - + let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:))) downSwipeGesture.direction = .down zoomView!.addGestureRecognizer(downSwipeGesture) - + let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:))) upSwipeGesture.direction = .up zoomView!.addGestureRecognizer(upSwipeGesture) - + configureImageForSize(image.size) adjustFrameToCenter() } - + private func configureImageForSize(_ size: CGSize) { imageSize = size contentSize = imageSize setMaxMinZoomScalesForCurrentBounds() zoomScale = minimumZoomScale - + switch initialOffset { case .beginning: contentOffset = CGPoint.zero case .center: let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2 let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2 - + switch imageContentMode { case .aspectFit: contentOffset = CGPoint.zero @@ -219,14 +219,14 @@ open class ImageScrollView: UIScrollView { } } } - + private func setMaxMinZoomScalesForCurrentBounds() { // calculate min/max zoomscale let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise - + var minScale: CGFloat = 1 - + switch imageContentMode { case .aspectFill: minScale = max(xScale, yScale) @@ -237,21 +237,20 @@ open class ImageScrollView: UIScrollView { case .heightFill: minScale = yScale } - - + let maxScale = maxScaleFromMinScale*minScale - + // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) if minScale > maxScale { minScale = maxScale } - + maximumZoomScale = maxScale minimumZoomScale = minScale // * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController } - + // MARK: - Gesture - + @objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { // zoom out if it bigger than middle scale point. Else, zoom in if zoomScale >= maximumZoomScale / 2.0 { @@ -262,96 +261,96 @@ open class ImageScrollView: UIScrollView { zoom(to: zoomRect, animated: true) } } - + @objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { if gestureRecognizer.state == .ended { imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self) } } - + @objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { if gestureRecognizer.state == .ended { imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self) } } - + private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { var zoomRect = CGRect.zero - + // the zoom rect is in the content view's coordinates. // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. // as the zoom scale decreases, so more content is visible, the size of the rect grows. zoomRect.size.height = frame.size.height / scale zoomRect.size.width = frame.size.width / scale - + // choose an origin so as to get the right center. zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) - + return zoomRect } - + open func refresh() { if let image = zoomView?.image { display(image: image) } } - + open func resize() { self.configureImageForSize(self.imageSize) } } extension ImageScrollView: UIScrollViewDelegate { - + public func scrollViewDidScroll(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewDidScroll?(scrollView) } - + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) } - + public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) } - + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) } - + public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView) } - + public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView) } - + public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) } - + public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view) } - + public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) } - + public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { return false } - + public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView) } - + public func viewForZooming(in scrollView: UIScrollView) -> UIView? { return zoomView } - + public func scrollViewDidZoom(_ scrollView: UIScrollView) { adjustFrameToCenter() imageScrollViewDelegate?.scrollViewDidZoom?(scrollView) diff --git a/iOS/Article/ImageTransition.swift b/iOS/Article/ImageTransition.swift index 25951b301..9575aecbf 100644 --- a/iOS/Article/ImageTransition.swift +++ b/iOS/Article/ImageTransition.swift @@ -16,15 +16,15 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { var originFrame: CGRect! var maskFrame: CGRect! var originImage: UIImage! - + init(controller: WebViewController) { self.webViewController = controller } - + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return duration } - + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { if presenting { animateTransitionPresenting(using: transitionContext) @@ -32,23 +32,23 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { animateTransitionReturning(using: transitionContext) } } - + private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) { let imageView = UIImageView(image: originImage) imageView.frame = originFrame - + let fromView = transitionContext.view(forKey: .from)! fromView.removeFromSuperview() transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor transitionContext.containerView.addSubview(imageView) - + webViewController?.hideClickedImage() UIView.animate( withDuration: duration, - delay:0.0, + delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, animations: { @@ -61,40 +61,40 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { transitionContext.completeTransition(true) }) } - + private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) { let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController let imageView = UIImageView(image: originImage) imageView.frame = imageController.zoomedFrame - + let fromView = transitionContext.view(forKey: .from)! let windowFrame = fromView.window!.frame fromView.removeFromSuperview() - + let toView = transitionContext.view(forKey: .to)! transitionContext.containerView.addSubview(toView) - + let maskingView = UIView() - + let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height) let path = UIBezierPath(rect: fullMaskFrame) let maskLayer = CAShapeLayer() maskLayer.path = path.cgPath maskingView.layer.mask = maskLayer - + maskingView.addSubview(imageView) transitionContext.containerView.addSubview(maskingView) UIView.animate( withDuration: duration, - delay:0.0, + delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, animations: { imageView.frame = self.originFrame }, completion: { _ in if let controller = self.webViewController { - controller.showClickedImage() { + controller.showClickedImage { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { imageView.removeFromSuperview() transitionContext.completeTransition(true) @@ -106,5 +106,5 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { } }) } - + } diff --git a/iOS/Article/ImageViewController.swift b/iOS/Article/ImageViewController.swift index 16f27a1ae..5896e46bd 100644 --- a/iOS/Article/ImageViewController.swift +++ b/iOS/Article/ImageViewController.swift @@ -17,7 +17,7 @@ final class ImageViewController: UIViewController { @IBOutlet weak var titleBackground: UIVisualEffectView! @IBOutlet weak var titleLeading: NSLayoutConstraint! @IBOutlet weak var titleTrailing: NSLayoutConstraint! - + var image: UIImage! var imageTitle: String? var zoomedFrame: CGRect { @@ -27,27 +27,27 @@ final class ImageViewController: UIViewController { init() { super.init(nibName: "ImageViewController", bundle: nil) } - + required init?(coder: NSCoder) { super.init(coder: coder) } - + override func viewDidLoad() { super.viewDidLoad() - + closeButton.imageView?.contentMode = .scaleAspectFit closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close") shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share") - + imageScrollView.setup() imageScrollView.imageScrollViewDelegate = self imageScrollView.imageContentMode = .aspectFit imageScrollView.initialOffset = .center imageScrollView.display(image: image) - + titleLabel.text = imageTitle ?? "" layoutTitleLabel() - + guard imageTitle != "" else { titleBackground.removeFromSuperview() return @@ -57,11 +57,11 @@ final class ImageViewController: UIViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: { [weak self] context in + coordinator.animate(alongsideTransition: { [weak self] _ in self?.imageScrollView.resize() }) } - + @IBAction func share(_ sender: Any) { guard let image = image else { return } let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) @@ -69,12 +69,12 @@ final class ImageViewController: UIViewController { activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds present(activityViewController, animated: true) } - + @IBAction func done(_ sender: Any) { dismiss(animated: true) } - - private func layoutTitleLabel(){ + + private func layoutTitleLabel() { let width = view.frame.width let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04) titleLeading.constant += width * multiplier @@ -90,10 +90,9 @@ extension ImageViewController: ImageScrollViewDelegate { func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) { dismiss(animated: true) } - + func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) { dismiss(animated: true) } - - + } diff --git a/iOS/Article/OpenInSafariActivity.swift b/iOS/Article/OpenInSafariActivity.swift index 2c9ae9eab..e70b6485c 100644 --- a/iOS/Article/OpenInSafariActivity.swift +++ b/iOS/Article/OpenInSafariActivity.swift @@ -9,17 +9,17 @@ import UIKit class OpenInBrowserActivity: UIActivity { - + private var activityItems: [Any]? override var activityTitle: String? { return NSLocalizedString("Open in Browser", comment: "Open in Browser") } - + override var activityImage: UIImage? { return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) } - + override var activityType: UIActivity.ActivityType? { return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari") } @@ -27,23 +27,23 @@ class OpenInBrowserActivity: UIActivity { override class var activityCategory: UIActivity.Category { return .action } - + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { return true } - + override func prepare(withActivityItems activityItems: [Any]) { self.activityItems = activityItems } - + override func perform() { guard let url = activityItems?.first(where: { $0 is URL }) as? URL else { activityDidFinish(false) return } - + UIApplication.shared.open(url, options: [:], completionHandler: nil) activityDidFinish(true) } - + } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index a6275831e..d2f0e5f09 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -30,7 +30,7 @@ final class WebViewController: UIViewController { private var bottomShowBarsView: UIView! private var topShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint! - + var webView: WKWebView? { return view.subviews[0] as? WKWebView } @@ -43,7 +43,7 @@ final class WebViewController: UIViewController { private lazy var transition = ImageTransition(controller: self) private var clickedImageCompletion: (() -> Void)? - private var articleExtractor: ArticleExtractor? = nil + private var articleExtractor: ArticleExtractor? var extractedArticle: ExtractedArticle? { didSet { windowScrollY = 0 @@ -56,12 +56,12 @@ final class WebViewController: UIViewController { delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState) } } - + weak var coordinator: SceneCoordinator! weak var delegate: WebViewControllerDelegate? - + private(set) var article: Article? - + let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) var windowScrollY = 0 private var restoreWindowScrollY: Int? @@ -77,13 +77,13 @@ final class WebViewController: UIViewController { // Configure the tap zones configureTopShowBarsView() configureBottomShowBarsView() - + loadWebView() } - + // MARK: Notifications - + @objc func feedIconDidBecomeAvailable(_ note: Notification) { reloadArticleImage() } @@ -101,16 +101,16 @@ final class WebViewController: UIViewController { } // MARK: Actions - + @objc func showBars(_ sender: Any) { showBars() } - + // MARK: API func setArticle(_ article: Article?, updateView: Bool = true) { stopArticleExtractor() - + if article != self.article { self.article = article if updateView { @@ -121,9 +121,9 @@ final class WebViewController: UIViewController { loadWebView() } } - + } - + func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { if isShowingExtractedArticle { switch articleExtractor?.state { @@ -144,7 +144,7 @@ final class WebViewController: UIViewController { loadWebView() } } - + func focus() { webView?.becomeFirstResponder() } @@ -164,7 +164,7 @@ final class WebViewController: UIViewController { let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale let scrollToY: CGFloat = { - let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap; + let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap let fullScroll = webView.scrollView.contentOffset.y + (scrollingUp ? -scrollDistance : scrollDistance) let final = finalScrollPosition(scrollingUp: scrollingUp) return (scrollingUp ? fullScroll > final : fullScroll < final) ? fullScroll : final @@ -186,12 +186,12 @@ final class WebViewController: UIViewController { func hideClickedImage() { webView?.evaluateJavaScript("hideClickedImage();") } - + func showClickedImage(completion: @escaping () -> Void) { clickedImageCompletion = completion webView?.evaluateJavaScript("showClickedImage();") } - + func fullReload() { loadWebView(replaceExistingWebView: true) } @@ -205,7 +205,7 @@ final class WebViewController: UIViewController { navigationController?.setToolbarHidden(false, animated: true) configureContextMenuInteraction() } - + func hideBars() { if isFullScreenAvailable { AppDefaults.shared.articleFullscreenEnabled = true @@ -248,7 +248,7 @@ final class WebViewController: UIViewController { } } - + func stopArticleExtractorIfProcessing() { if articleExtractor?.state == .processing { stopArticleExtractor() @@ -261,7 +261,7 @@ final class WebViewController: UIViewController { cancelImageLoad(webView) } } - + func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) { guard let url = article?.preferredURL else { return } let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) @@ -325,12 +325,12 @@ extension WebViewController: ArticleExtractorDelegate { extension WebViewController: UIContextMenuInteractionDelegate { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - - return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] suggestedActions in + + return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] _ in guard let self = self else { return nil } var menus = [UIMenu]() - + var navActions = [UIAction]() if let action = self.prevArticleAction() { navActions.append(action) @@ -341,7 +341,7 @@ extension WebViewController: UIContextMenuInteractionDelegate { if !navActions.isEmpty { menus.append(UIMenu(title: "", options: .displayInline, children: navActions)) } - + var toggleActions = [UIAction]() if let action = self.toggleReadAction() { toggleActions.append(action) @@ -355,29 +355,29 @@ extension WebViewController: UIContextMenuInteractionDelegate { menus.append(UIMenu(title: "", options: .displayInline, children: [self.toggleArticleExtractorAction()])) menus.append(UIMenu(title: "", options: .displayInline, children: [self.shareAction()])) - + return UIMenu(title: "", children: menus) } } - + func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { coordinator.showBrowserForCurrentArticle() } - + } // MARK: WKNavigationDelegate extension WebViewController: WKNavigationDelegate { - + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - + if navigationAction.navigationType == .linkActivated { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } - + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) if components?.scheme == "http" || components?.scheme == "https" { decisionHandler(.cancel) @@ -391,16 +391,16 @@ extension WebViewController: WKNavigationDelegate { self.openURLInSafariViewController(url) } } - + } else if components?.scheme == "mailto" { decisionHandler(.cancel) - + guard let emailAddress = url.percentEncodedEmailAddress else { return } - + if UIApplication.shared.canOpenURL(emailAddress) { - UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil) + UIApplication.shared.open(emailAddress, options: [.universalLinksOnly: false], completionHandler: nil) } else { let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) @@ -408,11 +408,11 @@ extension WebViewController: WKNavigationDelegate { } } else if components?.scheme == "tel" { decisionHandler(.cancel) - + if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) + UIApplication.shared.open(url, options: [.universalLinksOnly: false], completionHandler: nil) } - + } else { decisionHandler(.allow) } @@ -424,13 +424,13 @@ extension WebViewController: WKNavigationDelegate { func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { fullReload() } - + } // MARK: WKUIDelegate extension WebViewController: WKUIDelegate { - + func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { // We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the // link preview launch Safari when the link preview is tapped. In theory, you should be able to get @@ -442,11 +442,11 @@ extension WebViewController: WKUIDelegate { guard let url = navigationAction.request.url else { return nil } - + openURL(url) return nil } - + } // MARK: WKScriptMessageHandler @@ -467,7 +467,7 @@ extension WebViewController: WKScriptMessageHandler { return } } - + } // MARK: UIViewControllerTransitioningDelegate @@ -478,7 +478,7 @@ extension WebViewController: UIViewControllerTransitioningDelegate { transition.presenting = true return transition } - + func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.presenting = false return transition @@ -488,11 +488,11 @@ extension WebViewController: UIViewControllerTransitioningDelegate { // MARK: extension WebViewController: UIScrollViewDelegate { - + func scrollViewDidScroll(_ scrollView: UIScrollView) { scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) } - + @objc func scrollPositionDidChange() { webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in guard error == nil else { return } @@ -502,11 +502,9 @@ extension WebViewController: UIScrollViewDelegate { self.windowScrollY = javascriptScrollY } } - + } - - // MARK: JSON private struct ImageClickMessage: Codable { @@ -564,7 +562,7 @@ private extension WebViewController { func renderPage(_ webView: WKWebView?) { guard let webView = webView else { return } - + let theme = ArticleThemesManager.shared.currentTheme let rendering: ArticleRenderer.Rendering @@ -583,7 +581,7 @@ private extension WebViewController { } else { rendering = ArticleRenderer.noSelectionHTML(theme: theme) } - + let substitutions = [ "title": rendering.title, "baseURL": rendering.baseURL, @@ -595,7 +593,7 @@ private extension WebViewController { let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) } - + func finalScrollPosition(scrollingUp: Bool) -> CGFloat { guard let webView = webView else { return 0 } @@ -605,7 +603,7 @@ private extension WebViewController { return webView.scrollView.contentSize.height - webView.scrollView.bounds.height + webView.scrollView.safeAreaInsets.bottom } } - + func startArticleExtractor() { guard articleExtractor == nil else { return } if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { @@ -629,12 +627,12 @@ private extension WebViewController { var components = URLComponents() components.scheme = ArticleRenderer.imageIconScheme components.path = article.articleID - + if let imageSrc = components.string { webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") } } - + func imageWasClicked(body: String?) { guard let webView = webView, let body = body, @@ -642,22 +640,22 @@ private extension WebViewController { let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data), let range = clickMessage.imageURL.range(of: ";base64,") else { return } - + let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound)) if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) { - + let y = CGFloat(clickMessage.y) + webView.safeAreaInsets.top let rect = CGRect(x: CGFloat(clickMessage.x), y: y, width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height)) transition.originFrame = webView.convert(rect, to: nil) - + if navigationController?.navigationBar.isHidden ?? false { transition.maskFrame = webView.convert(webView.frame, to: nil) } else { transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil) } - + transition.originImage = image - + coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self) } } @@ -675,13 +673,13 @@ private extension WebViewController { topShowBarsView.backgroundColor = .clear topShowBarsView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(topShowBarsView) - + if AppDefaults.shared.logicalArticleFullscreenEnabled { topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0) } else { topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0) } - + NSLayoutConstraint.activate([ topShowBarsViewConstraint, view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor), @@ -690,7 +688,7 @@ private extension WebViewController { ]) topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) } - + func configureBottomShowBarsView() { bottomShowBarsView = UIView() topShowBarsView.backgroundColor = .clear @@ -709,7 +707,7 @@ private extension WebViewController { ]) bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) } - + func configureContextMenuInteraction() { if isFullScreenAvailable { if navigationController?.isNavigationBarHidden ?? false { @@ -719,33 +717,33 @@ private extension WebViewController { } } } - + func contextMenuPreviewProvider() -> UIViewController { ContextMenuPreviewViewController(article: article) } - + func prevArticleAction() -> UIAction? { guard coordinator.isPrevArticleAvailable else { return nil } let title = NSLocalizedString("Previous Article", comment: "Previous Article") - return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] action in + return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] _ in self?.coordinator.selectPrevArticle() } } - + func nextArticleAction() -> UIAction? { guard coordinator.isNextArticleAvailable else { return nil } let title = NSLocalizedString("Next Article", comment: "Next Article") - return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] action in + return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] _ in self?.coordinator.selectNextArticle() } } - + func toggleReadAction() -> UIAction? { guard let article = article, !article.status.read || article.isAvailableToMarkUnread else { return nil } - + let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read") let readImage = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage - return UIAction(title: title, image: readImage) { [weak self] action in + return UIAction(title: title, image: readImage) { [weak self] _ in self?.coordinator.toggleReadForCurrentArticle() } } @@ -754,7 +752,7 @@ private extension WebViewController { let starred = article?.status.starred ?? false let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred") let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage - return UIAction(title: title, image: starredImage) { [weak self] action in + return UIAction(title: title, image: starredImage) { [weak self] _ in self?.coordinator.toggleStarredForCurrentArticle() } } @@ -762,23 +760,23 @@ private extension WebViewController { func nextUnreadArticleAction() -> UIAction? { guard coordinator.isAnyUnreadAvailable else { return nil } let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article") - return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] action in + return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] _ in self?.coordinator.selectNextUnread() } } - + func toggleArticleExtractorAction() -> UIAction { let extracted = articleExtractorButtonState == .on let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View") let extractorImage = extracted ? AppAssets.articleExtractorOffSF : AppAssets.articleExtractorOnSF - return UIAction(title: title, image: extractorImage) { [weak self] action in + return UIAction(title: title, image: extractorImage) { [weak self] _ in self?.toggleArticleExtractor() } } func shareAction() -> UIAction { let title = NSLocalizedString("Share", comment: "Share") - return UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in + return UIAction(title: title, image: AppAssets.shareImage) { [weak self] _ in self?.showActivityDialog() } } @@ -816,27 +814,27 @@ internal struct FindInArticleState: Codable { let width: Double let height: Double } - + struct FindInArticleResult: Codable { let rects: [WebViewClientRect] let bounds: WebViewClientRect let index: UInt let matchGroups: [String] } - + let index: UInt? let results: [FindInArticleResult] let count: UInt } extension WebViewController { - + func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) { guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else { return } let encoded = json.base64EncodedString() - + webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") { (result, error) in guard error == nil, @@ -845,21 +843,21 @@ extension WebViewController { let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else { return } - + completionHandler(findState) } } - + func endSearch() { webView?.evaluateJavaScript("endFind()") } - + func selectNextSearchResult() { webView?.evaluateJavaScript("selectNextResult()") } - + func selectPreviousSearchResult() { webView?.evaluateJavaScript("selectPreviousResult()") } - + } diff --git a/iOS/Article/WrapperScriptMessageHandler.swift b/iOS/Article/WrapperScriptMessageHandler.swift index a12c606ce..0baa51ca4 100644 --- a/iOS/Article/WrapperScriptMessageHandler.swift +++ b/iOS/Article/WrapperScriptMessageHandler.swift @@ -10,16 +10,16 @@ import Foundation import WebKit class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler { - + // We need to wrap a message handler to prevent a circlular reference private weak var handler: WKScriptMessageHandler? - + init(_ handler: WKScriptMessageHandler) { self.handler = handler } - + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { handler?.userContentController(userContentController, didReceive: message) } - + } diff --git a/iOS/ArticleActivityItemSource.swift b/iOS/ArticleActivityItemSource.swift index ba168651e..290e99aca 100644 --- a/iOS/ArticleActivityItemSource.swift +++ b/iOS/ArticleActivityItemSource.swift @@ -9,25 +9,25 @@ import UIKit class ArticleActivityItemSource: NSObject, UIActivityItemSource { - + private let url: URL private let subject: String? - + init(url: URL, subject: String?) { self.url = url self.subject = subject } - - func activityViewControllerPlaceholderItem(_ : UIActivityViewController) -> Any { + + func activityViewControllerPlaceholderItem(_: UIActivityViewController) -> Any { return url } - + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { return url } - + func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { return subject ?? "" } - + } diff --git a/iOS/ErrorHandler.swift b/iOS/ErrorHandler.swift index 64957ad3b..35cf9f53a 100644 --- a/iOS/ErrorHandler.swift +++ b/iOS/ErrorHandler.swift @@ -14,7 +14,7 @@ struct ErrorHandler { private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") - public static func present(_ viewController: UIViewController) -> (Error) -> () { + public static func present(_ viewController: UIViewController) -> (Error) -> Void { return { [weak viewController] error in if UIApplication.shared.applicationState == .active { viewController?.presentError(error) @@ -23,7 +23,7 @@ struct ErrorHandler { } } } - + public static func log(_ error: Error) { os_log(.error, log: self.log, "%@", error.localizedDescription) } diff --git a/iOS/IconView.swift b/iOS/IconView.swift index 35f95daeb..120272c41 100644 --- a/iOS/IconView.swift +++ b/iOS/IconView.swift @@ -10,7 +10,7 @@ import UIKit final class IconView: UIView { - var iconImage: IconImage? = nil { + var iconImage: IconImage? { didSet { guard iconImage !== oldValue else { return @@ -19,8 +19,7 @@ final class IconView: UIView { if traitCollection.userInterfaceStyle == .dark { let isDark = iconImage?.isDark ?? false isDiscernable = !isDark - } - else { + } else { let isBright = iconImage?.isBright ?? false isDiscernable = !isBright } @@ -45,11 +44,11 @@ final class IconView: UIView { private var isSymbolImage: Bool { return iconImage?.isSymbol ?? false } - + private var isBackgroundSuppressed: Bool { return iconImage?.isBackgroundSuppressed ?? false } - + override init(frame: CGRect) { super.init(frame: frame) commonInit() @@ -59,7 +58,7 @@ final class IconView: UIView { super.init(coder: coder) commonInit() } - + convenience init() { self.init(frame: .zero) } @@ -96,8 +95,7 @@ private extension IconView { } let offset = floor((viewSize.height - imageSize.height) / 2.0) return CGRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height) - } - else if imageSize.height > imageSize.width { + } else if imageSize.height > imageSize.width { let factor = viewSize.height / imageSize.height let width = imageSize.width * factor let originX = floor((viewSize.width - width) / 2.0) diff --git a/iOS/Inspector/AccountInspectorViewController.swift b/iOS/Inspector/AccountInspectorViewController.swift index 6a8366f48..d553756c1 100644 --- a/iOS/Inspector/AccountInspectorViewController.swift +++ b/iOS/Inspector/AccountInspectorViewController.swift @@ -21,23 +21,23 @@ class AccountInspectorViewController: UITableViewController { var isModal = false weak var account: Account? - + override func viewDidLoad() { super.viewDidLoad() - + guard let account = account else { return } - + nameTextField.placeholder = account.defaultName nameTextField.text = account.name nameTextField.delegate = self activeSwitch.isOn = account.isActive - + navigationItem.title = account.nameForDisplay - + if account.type != .onMyMac { - deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal) + deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal) } - + if account.type != .cloudKit { limitationsAndSolutionsButton.isHidden = true } @@ -46,11 +46,11 @@ class AccountInspectorViewController: UITableViewController { let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) navigationItem.leftBarButtonItem = doneBarButtonItem } - + tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") } - + override func viewWillDisappear(_ animated: Bool) { account?.name = nameTextField.text account?.isActive = activeSwitch.isOn @@ -59,7 +59,7 @@ class AccountInspectorViewController: UITableViewController { @objc func done() { dismiss(animated: true) } - + @IBAction func credentials(_ sender: Any) { guard let account = account else { return } switch account.type { @@ -86,12 +86,12 @@ class AccountInspectorViewController: UITableViewController { break } } - + @IBAction func deleteAccount(_ sender: Any) { guard let account = account else { return } - + let title = NSLocalizedString("Remove Account", comment: "Remove Account") let message: String = { switch account.type { @@ -105,9 +105,9 @@ class AccountInspectorViewController: UITableViewController { let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) alertController.addAction(cancelAction) - + let markTitle = NSLocalizedString("Remove", comment: "Remove") - let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in + let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (_) in guard let self = self, let account = self.account else { return } AccountManager.shared.deleteAccount(account) if self.isModal { @@ -118,7 +118,7 @@ class AccountInspectorViewController: UITableViewController { } alertController.addAction(markAction) alertController.preferredAction = markAction - + present(alertController, animated: true) } @@ -132,7 +132,7 @@ class AccountInspectorViewController: UITableViewController { // MARK: Table View extension AccountInspectorViewController { - + var hidesCredentialsSection: Bool { guard let account = account else { return true @@ -147,7 +147,7 @@ extension AccountInspectorViewController { override func numberOfSections(in tableView: UITableView) -> Int { guard let account = account else { return 0 } - + if account == AccountManager.shared.defaultAccount { return 1 } else if hidesCredentialsSection { @@ -156,11 +156,11 @@ extension AccountInspectorViewController { return super.numberOfSections(in: tableView) } } - + 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 account = account else { return nil } @@ -172,16 +172,16 @@ extension AccountInspectorViewController { return super.tableView(tableView, viewForHeaderInSection: section) } } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UITableViewCell - + if indexPath.section == 1, hidesCredentialsSection { cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2)) } else { cell = super.tableView(tableView, cellForRowAt: indexPath) } - + return cell } @@ -191,17 +191,17 @@ extension AccountInspectorViewController { } return false } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.selectRow(at: nil, animated: true, scrollPosition: .none) } - + } // MARK: UITextFieldDelegate extension AccountInspectorViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true diff --git a/iOS/Inspector/FeedInspectorViewController.swift b/iOS/Inspector/FeedInspectorViewController.swift index 0c1080bed..2e09b8973 100644 --- a/iOS/Inspector/FeedInspectorViewController.swift +++ b/iOS/Inspector/FeedInspectorViewController.swift @@ -12,52 +12,52 @@ import SafariServices import UserNotifications class FeedInspectorViewController: UITableViewController { - + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0) - + var feed: Feed! @IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch! @IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch! @IBOutlet weak var homePageLabel: InteractiveLabel! @IBOutlet weak var feedURLLabel: InteractiveLabel! - + private var headerView: InspectorIconHeaderView? private var iconImage: IconImage? { return IconImageCache.shared.imageForFeed(feed) } - + private let homePageIndexPath = IndexPath(row: 0, section: 1) - + private var shouldHideHomePageSection: Bool { return feed.homePageURL == nil } - + private var userNotificationSettings: UNNotificationSettings? - + override func viewDidLoad() { tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - + navigationItem.title = feed.nameForDisplay nameTextField.text = feed.nameForDisplay - + notifyAboutNewArticlesSwitch.setOn(feed.isNotifyAboutNewArticles ?? false, animated: false) - + alwaysShowReaderViewSwitch.setOn(feed.isArticleExtractorAlwaysOn ?? false, animated: false) homePageLabel.text = feed.homePageURL feedURLLabel.text = feed.url - + NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil) - + } - + override func viewDidAppear(_ animated: Bool) { updateNotificationSettings() } - + override func viewDidDisappear(_ animated: Bool) { if nameTextField.text != feed.nameForDisplay { let nameText = nameTextField.text ?? "" @@ -65,12 +65,12 @@ class FeedInspectorViewController: UITableViewController { feed.rename(to: newName) { _ in } } } - + // MARK: Notifications @objc func feedIconDidBecomeAvailable(_ notification: Notification) { headerView?.iconView.iconImage = iconImage } - + @IBAction func notifyAboutNewArticlesChanged(_ sender: Any) { guard let settings = userNotificationSettings else { notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn @@ -82,7 +82,7 @@ class FeedInspectorViewController: UITableViewController { } else if settings.authorizationStatus == .authorized { feed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn } else { - UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in + UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in self.updateNotificationSettings() if granted { DispatchQueue.main.async { @@ -97,22 +97,22 @@ class FeedInspectorViewController: UITableViewController { } } } - + @IBAction func alwaysShowReaderViewChanged(_ sender: Any) { feed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn } - + @IBAction func done(_ sender: Any) { dismiss(animated: true) } - + /// Returns a new indexPath, taking into consideration any /// conditions that may require the tableView to be /// displayed differently than what is setup in the storyboard. private func shift(_ indexPath: IndexPath) -> IndexPath { return IndexPath(row: indexPath.row, section: shift(indexPath.section)) } - + /// Returns a new section, taking into consideration any /// conditions that may require the tableView to be /// displayed differently than what is setup in the storyboard. @@ -123,7 +123,6 @@ class FeedInspectorViewController: UITableViewController { return section } - } // MARK: Table View @@ -134,15 +133,15 @@ extension FeedInspectorViewController { let numberOfSections = super.numberOfSections(in: tableView) return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return super.tableView(tableView, numberOfRowsInSection: shift(section)) } - + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section)) } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: shift(indexPath)) if indexPath.section == 0 && indexPath.row == 1 { @@ -154,11 +153,11 @@ extension FeedInspectorViewController { } return cell } - + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { super.tableView(tableView, titleForHeaderInSection: shift(section)) } - + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { if shift(section) == 0 { headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView @@ -168,12 +167,12 @@ extension FeedInspectorViewController { return super.tableView(tableView, viewForHeaderInSection: shift(section)) } } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if shift(indexPath) == homePageIndexPath, let homePageUrlString = feed.homePageURL, let homePageUrl = URL(string: homePageUrlString) { - + let safari = SFSafariViewController(url: homePageUrl) safari.modalPresentationStyle = .pageSheet present(safari, animated: true) { @@ -181,24 +180,24 @@ extension FeedInspectorViewController { } } } - + } // MARK: UITextFieldDelegate extension FeedInspectorViewController: UITextFieldDelegate { - + func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } - + } // MARK: UNUserNotificationCenter extension FeedInspectorViewController { - + @objc func updateNotificationSettings() { UNUserNotificationCenter.current().getNotificationSettings { (settings) in @@ -210,12 +209,12 @@ extension FeedInspectorViewController { } } } - + func notificationUpdateErrorAlert() -> UIAlertController { let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"), message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert) - let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (action) in - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) + let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (_) in + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: false], completionHandler: nil) } let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil) alert.addAction(openSettings) @@ -223,5 +222,5 @@ extension FeedInspectorViewController { alert.preferredAction = openSettings return alert } - + } diff --git a/iOS/Inspector/InspectorIconHeaderView.swift b/iOS/Inspector/InspectorIconHeaderView.swift index e0e45f559..b7870bcd6 100644 --- a/iOS/Inspector/InspectorIconHeaderView.swift +++ b/iOS/Inspector/InspectorIconHeaderView.swift @@ -11,17 +11,17 @@ import UIKit class InspectorIconHeaderView: UITableViewHeaderFooterView { var iconView = IconView() - + override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) commonInit() } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + func commonInit() { addSubview(iconView) } diff --git a/iOS/IntentsExtension/IntentHandler.swift b/iOS/IntentsExtension/IntentHandler.swift index e1c80701f..612a9fdbf 100644 --- a/iOS/IntentsExtension/IntentHandler.swift +++ b/iOS/IntentsExtension/IntentHandler.swift @@ -9,7 +9,7 @@ import Intents class IntentHandler: INExtension { - + override func handler(for intent: INIntent) -> Any { switch intent { case is AddWebFeedIntent: @@ -18,5 +18,5 @@ class IntentHandler: INExtension { fatalError("Unhandled intent type: \(intent)") } } - + } diff --git a/iOS/KeyboardManager.swift b/iOS/KeyboardManager.swift index b73334d1a..d4f7803c5 100644 --- a/iOS/KeyboardManager.swift +++ b/iOS/KeyboardManager.swift @@ -16,16 +16,16 @@ enum KeyboardType: String { } class KeyboardManager { - + private(set) var _keyCommands: [UIKeyCommand] var keyCommands: [UIKeyCommand] { guard !UIResponder.isFirstResponderTextField else { return [UIKeyCommand]() } return _keyCommands } - + init(type: KeyboardType) { _keyCommands = KeyboardManager.globalAuxilaryKeyCommands() - + switch type { case .sidebar: _keyCommands.append(contentsOf: KeyboardManager.hardcodeFeedKeyCommands()) @@ -34,7 +34,7 @@ class KeyboardManager { default: break } - + let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")! let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]] let globalCommands = globalEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) } @@ -42,9 +42,9 @@ class KeyboardManager { let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")! let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]] - _keyCommands.append(contentsOf: specificEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) } ) + _keyCommands.append(contentsOf: specificEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) }) } - + static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand { let selector = NSSelectorFromString(action) let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on) @@ -54,7 +54,7 @@ class KeyboardManager { } private extension KeyboardManager { - + static func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? { guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil } let modifiers = createKeyModifierFlags(keyEntry: keyEntry) @@ -71,8 +71,8 @@ private extension KeyboardManager { static func createKeyCommandInput(keyEntry: [String: Any]) -> String? { guard let key = keyEntry["key"] as? String else { return nil } - - switch(key) { + + switch key { case "[space]": return "\u{0020}" case "[uparrow]": @@ -96,34 +96,34 @@ private extension KeyboardManager { default: return key } - + } - + static func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags { var flags = UIKeyModifierFlags() - + if keyEntry["shiftModifier"] as? Bool ?? false { flags.insert(.shift) } - + if keyEntry["optionModifier"] as? Bool ?? false { flags.insert(.alternate) } - + if keyEntry["commandModifier"] as? Bool ?? false { flags.insert(.command) } - + if keyEntry["controlModifier"] as? Bool ?? false { flags.insert(.control) } return flags } - + static func globalAuxilaryKeyCommands() -> [UIKeyCommand] { var keys = [UIKeyCommand]() - + let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed") keys.append(KeyboardManager.createKeyCommand(title: addNewFeedTitle, action: "addNewFeed:", input: "n", modifiers: [.command])) @@ -147,7 +147,7 @@ private extension KeyboardManager { let gotoSettings = NSLocalizedString("Go To Settings", comment: "Go To Settings") keys.append(KeyboardManager.createKeyCommand(title: gotoSettings, action: "goToSettings:", input: ",", modifiers: [.command])) - + let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search") keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate])) @@ -165,7 +165,7 @@ private extension KeyboardManager { return keys } - + static func hardcodeFeedKeyCommands() -> [UIKeyCommand] { var keys = [UIKeyCommand]() @@ -174,16 +174,16 @@ private extension KeyboardManager { let nextDownTitle = NSLocalizedString("Select Next Down", comment: "Select Next Down") keys.append(KeyboardManager.createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: [])) - + let getFeedInfo = NSLocalizedString("Get Feed Info", comment: "Get Feed Info") keys.append(KeyboardManager.createKeyCommand(title: getFeedInfo, action: "showFeedInspector:", input: "i", modifiers: .command)) return keys } - + static func hardcodeArticleKeyCommands() -> [UIKeyCommand] { var keys = [UIKeyCommand]() - + let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser") keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command])) @@ -198,7 +198,7 @@ private extension KeyboardManager { let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status") keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift])) - + let findInArticleTitle = NSLocalizedString("Find in Article", comment: "Find in Article") keys.append(KeyboardManager.createKeyCommand(title: findInArticleTitle, action: "beginFind:", input: "f", modifiers: [.command])) @@ -213,5 +213,5 @@ private extension KeyboardManager { return keys } - + } diff --git a/iOS/MainFeed/Cell/MainFeedRowIdentifier.swift b/iOS/MainFeed/Cell/MainFeedRowIdentifier.swift index 7cffecb6e..b20857e78 100644 --- a/iOS/MainFeed/Cell/MainFeedRowIdentifier.swift +++ b/iOS/MainFeed/Cell/MainFeedRowIdentifier.swift @@ -11,13 +11,13 @@ import Foundation class MainFeedRowIdentifier: NSObject, NSCopying { var indexPath: IndexPath - + init(indexPath: IndexPath) { self.indexPath = indexPath } - + func copy(with zone: NSZone? = nil) -> Any { return self } - + } diff --git a/iOS/MainFeed/Cell/MainFeedTableViewCell.swift b/iOS/MainFeed/Cell/MainFeedTableViewCell.swift index e90ba3b65..19d7ce730 100644 --- a/iOS/MainFeed/Cell/MainFeedTableViewCell.swift +++ b/iOS/MainFeed/Cell/MainFeedTableViewCell.swift @@ -15,7 +15,7 @@ protocol MainFeedTableViewCellDelegate: AnyObject { func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool) } -class MainFeedTableViewCell : VibrantTableViewCell { +class MainFeedTableViewCell: VibrantTableViewCell { weak var delegate: MainFeedTableViewCellDelegate? @@ -44,7 +44,7 @@ class MainFeedTableViewCell : VibrantTableViewCell { } } } - + var isSeparatorShown = true { didSet { if isSeparatorShown != oldValue { @@ -56,7 +56,7 @@ class MainFeedTableViewCell : VibrantTableViewCell { } } } - + var unreadCount: Int { get { return unreadCountView.unreadCount @@ -100,17 +100,17 @@ class MainFeedTableViewCell : VibrantTableViewCell { view.alpha = 0.5 return view }() - + private var isDisclosureExpanded = false private var disclosureButton: UIButton? private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) private var isShowingEditControl = false - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + func setDisclosure(isExpanded: Bool, animated: Bool) { isDisclosureExpanded = isExpanded let duration = animated ? 0.3 : 0.0 @@ -120,42 +120,38 @@ class MainFeedTableViewCell : VibrantTableViewCell { self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder") self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796) } else { - self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder") + self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder") self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0) } } } - - override func applyThemeProperties() { - super.applyThemeProperties() - } override func willTransition(to state: UITableViewCell.StateMask) { super.willTransition(to: state) isShowingEditControl = state.contains(.showingEditControl) } - + override func sizeThatFits(_ size: CGSize) -> CGSize { let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable) return CGSize(width: bounds.width, height: layout.height) } - + override func layoutSubviews() { super.layoutSubviews() let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable) layoutWith(layout) } - + @objc func buttonPressed(_ sender: UIButton) { if isDisclosureAvailable { setDisclosure(isExpanded: !isDisclosureExpanded, animated: true) delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded) } } - + override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) - + let iconTintColor: UIColor if isHighlighted || isSelected { iconTintColor = AppAssets.vibrantTextColor @@ -166,7 +162,7 @@ class MainFeedTableViewCell : VibrantTableViewCell { iconTintColor = AppAssets.secondaryAccentColor } } - + if animated { UIView.animate(withDuration: Self.duration) { self.iconView.tintColor = iconTintColor @@ -174,10 +170,10 @@ class MainFeedTableViewCell : VibrantTableViewCell { } else { self.iconView.tintColor = iconTintColor } - + updateLabelVibrancy(titleView, color: labelColor, animated: animated) } - + } private extension MainFeedTableViewCell { @@ -200,7 +196,7 @@ private extension MainFeedTableViewCell { disclosureButton?.addInteraction(UIPointerInteraction()) addSubviewAtInit(disclosureButton!) } - + func addSubviewAtInit(_ view: UIView) { addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false @@ -220,11 +216,11 @@ private extension MainFeedTableViewCell { view.isHidden = true } } - + func showView(_ view: UIView) { if view.isHidden { view.isHidden = false } } - + } diff --git a/iOS/MainFeed/Cell/MainFeedTableViewCellLayout.swift b/iOS/MainFeed/Cell/MainFeedTableViewCellLayout.swift index 7d00b4752..303c784c4 100644 --- a/iOS/MainFeed/Cell/MainFeedTableViewCellLayout.swift +++ b/iOS/MainFeed/Cell/MainFeedTableViewCellLayout.swift @@ -21,7 +21,7 @@ struct MainFeedTableViewCellLayout { private static let verticalPadding = CGFloat(integerLiteral: 11) private static let minRowHeight = CGFloat(integerLiteral: 44) - + static let faviconCornerRadius = CGFloat(integerLiteral: 2) let faviconRect: CGRect @@ -29,9 +29,9 @@ struct MainFeedTableViewCellLayout { let unreadCountRect: CGRect let disclosureButtonRect: CGRect let separatorRect: CGRect - + let height: CGFloat - + init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) { var initialIndent = insets.left @@ -39,7 +39,7 @@ struct MainFeedTableViewCellLayout { initialIndent += MainFeedTableViewCellLayout.indentWidth } let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0) - + // Disclosure Button var rDisclosure = CGRect.zero if shouldShowDisclosure { @@ -66,7 +66,7 @@ struct MainFeedTableViewCellLayout { rUnread.size = unreadCountSize rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width) } - + // Title var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width if !shouldShowDisclosure { @@ -80,7 +80,7 @@ struct MainFeedTableViewCellLayout { } else { labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight) } - + let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) // Now that we've got everything (especially the label) computed without the editing controls, update for them. @@ -99,7 +99,7 @@ struct MainFeedTableViewCellLayout { } var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) - + // Determine cell height let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding) let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY() @@ -107,7 +107,7 @@ struct MainFeedTableViewCellLayout { if cellHeight < MainFeedTableViewCellLayout.minRowHeight { cellHeight = MainFeedTableViewCellLayout.minRowHeight } - + // Center in Cell let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight) if !unreadCountIsHidden { @@ -126,16 +126,16 @@ struct MainFeedTableViewCellLayout { // Separator Insets let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5) - + // Assign the properties self.height = cellHeight self.faviconRect = rFavicon self.unreadCountRect = rUnread self.disclosureButtonRect = rDisclosure self.titleRect = rLabel - + } - + // Ideally this will be implemented in RSCore (see RSGeometry) static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect { var result = originalRect @@ -144,5 +144,5 @@ struct MainFeedTableViewCellLayout { result.size = originalRect.size return result } - + } diff --git a/iOS/MainFeed/Cell/MainFeedTableViewSectionHeader.swift b/iOS/MainFeed/Cell/MainFeedTableViewSectionHeader.swift index 5041cc528..00941427e 100644 --- a/iOS/MainFeed/Cell/MainFeedTableViewSectionHeader.swift +++ b/iOS/MainFeed/Cell/MainFeedTableViewSectionHeader.swift @@ -15,7 +15,7 @@ protocol MainFeedTableViewSectionHeaderDelegate { class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { var delegate: MainFeedTableViewSectionHeaderDelegate? - + override var accessibilityLabel: String? { set {} get { @@ -37,7 +37,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility") } } - + var unreadCount: Int { get { return unreadCountView.unreadCount @@ -50,7 +50,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { } } } - + var name: String { get { return titleView.text ?? "" @@ -62,16 +62,16 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { } } } - + var disclosureExpanded = false { didSet { updateExpandedState(animate: true) updateUnreadCountView() } } - + var isLastSection = false - + private let titleView: UILabel = { let label = NonIntrinsicLabel() label.numberOfLines = 0 @@ -80,7 +80,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { label.font = .preferredFont(forTextStyle: .body) return label }() - + private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) private lazy var disclosureButton: UIButton = { @@ -98,27 +98,27 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { view.backgroundColor = UIColor.separator return view }() - + private let bottomSeparatorView: UIView = { let view = UIView() view.backgroundColor = UIColor.separator return view }() - + override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) commonInit() } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + override func sizeThatFits(_ size: CGSize) -> CGSize { let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView) return CGSize(width: bounds.width, height: layout.height) - + } override func layoutSubviews() { @@ -137,7 +137,7 @@ private extension MainFeedTableViewSectionHeader { @objc func toggleDisclosure() { delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self) } - + func commonInit() { addSubviewAtInit(unreadCountView) addSubviewAtInit(titleView) @@ -147,14 +147,14 @@ private extension MainFeedTableViewSectionHeader { addSubviewAtInit(topSeparatorView) addSubviewAtInit(bottomSeparatorView) } - + func updateExpandedState(animate: Bool) { if !isLastSection && self.disclosureExpanded { self.bottomSeparatorView.isHidden = false } - + let duration = animate ? 0.3 : 0.0 - + UIView.animate( withDuration: duration, animations: { @@ -169,7 +169,7 @@ private extension MainFeedTableViewSectionHeader { } }) } - + func updateUnreadCountView() { if !disclosureExpanded && unreadCount > 0 { UIView.animate(withDuration: 0.3) { @@ -186,7 +186,7 @@ private extension MainFeedTableViewSectionHeader { contentView.addSubview(view) view.translatesAutoresizingMaskIntoConstraints = false } - + func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) { titleView.setFrameIfNotEqual(layout.titleRect) unreadCountView.setFrameIfNotEqual(layout.unreadCountRect) @@ -198,14 +198,14 @@ private extension MainFeedTableViewSectionHeader { let top = CGRect(x: x, y: 0, width: width, height: height) topSeparatorView.setFrameIfNotEqual(top) - + let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height) bottomSeparatorView.setFrameIfNotEqual(bottom) } - + func addBackgroundView() { self.backgroundView = UIView(frame: self.bounds) self.backgroundView?.backgroundColor = AppAssets.sectionHeaderColor } - + } diff --git a/iOS/MainFeed/Cell/MainFeedTableViewSectionHeaderLayout.swift b/iOS/MainFeed/Cell/MainFeedTableViewSectionHeaderLayout.swift index 2905dae54..7826885d1 100644 --- a/iOS/MainFeed/Cell/MainFeedTableViewSectionHeaderLayout.swift +++ b/iOS/MainFeed/Cell/MainFeedTableViewSectionHeaderLayout.swift @@ -17,17 +17,17 @@ struct MainFeedTableViewSectionHeaderLayout { private static let verticalPadding = CGFloat(integerLiteral: 11) private static let minRowHeight = CGFloat(integerLiteral: 44) - + let titleRect: CGRect let unreadCountRect: CGRect let disclosureButtonRect: CGRect - + let height: CGFloat - + init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView) { let bounds = CGRect(x: insets.left, y: 0.0, width: floor(cellWidth - insets.right), height: 0.0) - + // Disclosure Button var rDisclosure = CGRect.zero rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize @@ -42,7 +42,7 @@ struct MainFeedTableViewSectionHeaderLayout { rUnread.size = unreadCountSize rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width) } - + // Max Unread Count // We can't reload Section Headers so we don't let the title extend into the (probably) worse case Unread Count area. let maxUnreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) @@ -58,7 +58,7 @@ struct MainFeedTableViewSectionHeaderLayout { let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) - + // Determine cell height let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding) let maxGraphicsHeight = [rUnread, rDisclosure].maxY() @@ -66,7 +66,7 @@ struct MainFeedTableViewSectionHeaderLayout { if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight { cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight } - + // Center in Cell let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight) if !unreadCountIsHidden { @@ -78,13 +78,13 @@ struct MainFeedTableViewSectionHeaderLayout { if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight { rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds) } - + // Assign the properties self.height = cellHeight self.unreadCountRect = rUnread self.disclosureButtonRect = rDisclosure self.titleRect = rLabel - + } - + } diff --git a/iOS/MainFeed/Cell/MainFeedUnreadCountView.swift b/iOS/MainFeed/Cell/MainFeedUnreadCountView.swift index c6dc46414..b59a9a10b 100644 --- a/iOS/MainFeed/Cell/MainFeedUnreadCountView.swift +++ b/iOS/MainFeed/Cell/MainFeedUnreadCountView.swift @@ -8,18 +8,18 @@ import UIKit -class MainFeedUnreadCountView : UIView { +class MainFeedUnreadCountView: UIView { var padding: UIEdgeInsets { return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0) } - + let cornerRadius = 8.0 let bgColor = AppAssets.controlBackgroundColor var textColor: UIColor { return UIColor.white } - + var textAttributes: [NSAttributedString.Key: AnyObject] { let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold() return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()] @@ -33,7 +33,7 @@ class MainFeedUnreadCountView : UIView { setNeedsDisplay() } } - + var unreadCountString: String { return unreadCount < 1 ? "" : "\(unreadCount)" } @@ -45,18 +45,18 @@ class MainFeedUnreadCountView : UIView { super.init(frame: frame) self.isOpaque = false } - + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.isOpaque = false } - + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { textSizeCache = [Int: CGSize]() contentSizeIsValid = false setNeedsDisplay() } - + var contentSize: CGSize { if !contentSizeIsValid { var size = CGSize.zero @@ -70,7 +70,7 @@ class MainFeedUnreadCountView : UIView { } return _contentSize } - + // Prevent autolayout from messing around with our frame settings override var intrinsicContentSize: CGSize { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) @@ -92,7 +92,7 @@ class MainFeedUnreadCountView : UIView { textSizeCache[unreadCount] = size return size - + } func textRect() -> CGRect { @@ -103,7 +103,7 @@ class MainFeedUnreadCountView : UIView { r.origin.x = (bounds.maxX - padding.right) - r.size.width r.origin.y = padding.top return r - + } override func draw(_ dirtyRect: CGRect) { @@ -116,8 +116,7 @@ class MainFeedUnreadCountView : UIView { if unreadCount > 0 { unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes) } - - } - -} + } + +} diff --git a/iOS/MainFeed/MainFeedViewController+Drag.swift b/iOS/MainFeed/MainFeedViewController+Drag.swift index 0a07f0912..e6e30bee0 100644 --- a/iOS/MainFeed/MainFeedViewController+Drag.swift +++ b/iOS/MainFeed/MainFeedViewController+Drag.swift @@ -12,25 +12,25 @@ import Account import UniformTypeIdentifiers extension MainFeedViewController: UITableViewDragDelegate { - + func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else { return [UIDragItem]() } - + let data = feed.url.data(using: .utf8) let itemProvider = NSItemProvider() - + itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.url.identifier, visibility: .ownProcess) { completion in Task { @MainActor in completion(data, nil) } return nil } - + let dragItem = UIDragItem(itemProvider: itemProvider) dragItem.localObject = node return [dragItem] } - + } diff --git a/iOS/MainFeed/MainFeedViewController+Drop.swift b/iOS/MainFeed/MainFeedViewController+Drop.swift index 7507dd162..33fc49054 100644 --- a/iOS/MainFeed/MainFeedViewController+Drop.swift +++ b/iOS/MainFeed/MainFeedViewController+Drop.swift @@ -12,16 +12,16 @@ import Account import RSTree extension MainFeedViewController: UITableViewDropDelegate { - + func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { return session.localDragSession != nil } - + func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { - guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else { + guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else { return UITableViewDropProposal(operation: .forbidden) } - + guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem, let destAccount = destFeed.account, let destCell = tableView.cellForRow(at: destIndexPath) else { @@ -48,7 +48,7 @@ extension MainFeedViewController: UITableViewDropDelegate { } } - + func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { guard let dragItem = dropCoordinator.items.first?.dragItem, let dragNode = dragItem.localObject as? Node, @@ -56,17 +56,17 @@ extension MainFeedViewController: UITableViewDropDelegate { let destIndexPath = dropCoordinator.destinationIndexPath else { return } - + let isFolderDrop: Bool = { if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) { return dropCoordinator.session.location(in: propCell).y >= 0 } return false }() - + // Based on the drop we have to determine a node to start looking for a parent container. let destNode: Node? = { - + if isFolderDrop { return coordinator.nodeFor(destIndexPath) } else { @@ -78,7 +78,7 @@ extension MainFeedViewController: UITableViewDropDelegate { return nil } } - + }() // Now we start looking for the parent container @@ -90,9 +90,9 @@ extension MainFeedViewController: UITableViewDropDelegate { return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account } }() - + guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return } - + if source.account == destination.account { moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination) } else { @@ -102,7 +102,7 @@ extension MainFeedViewController: UITableViewDropDelegate { func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) { guard sourceContainer !== destinationContainer else { return } - + BatchUpdate.shared.start() sourceContainer.account?.moveFeed(feed, from: sourceContainer, to: destinationContainer) { result in BatchUpdate.shared.end() @@ -114,11 +114,11 @@ extension MainFeedViewController: UITableViewDropDelegate { } } } - + func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) { - + if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) { - + BatchUpdate.shared.start() destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in switch result { @@ -137,9 +137,9 @@ extension MainFeedViewController: UITableViewDropDelegate { self.presentError(error) } } - + } else { - + BatchUpdate.shared.start() destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in switch result { @@ -158,9 +158,8 @@ extension MainFeedViewController: UITableViewDropDelegate { self.presentError(error) } } - + } } - } diff --git a/iOS/MainFeed/MainFeedViewController.swift b/iOS/MainFeed/MainFeedViewController.swift index b8f481ac2..3d22ad32e 100644 --- a/iOS/MainFeed/MainFeedViewController.swift +++ b/iOS/MainFeed/MainFeedViewController.swift @@ -125,7 +125,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { return } - var node: Node? = nil + var node: Node? if let coordinator = unreadCountProvider as? SceneCoordinator, let feed = coordinator.timelineFeed { node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) } else { @@ -249,7 +249,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { } headerView.gestureRecognizers?.removeAll() - let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:))) + let tap = UITapGestureRecognizer(target: self, action: #selector(self.toggleSectionHeader(_:))) headerView.addGestureRecognizer(tap) // Without this the swipe gesture registers on the cell below @@ -279,7 +279,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { // Set up the delete action let deleteTitle = NSLocalizedString("Delete", comment: "Delete") - let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in + let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (_, _, completion) in self?.delete(indexPath: indexPath) completion(true) } @@ -288,7 +288,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { // Set up the rename action let renameTitle = NSLocalizedString("Rename", comment: "Rename") - let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (action, view, completion) in + let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (_, _, completion) in self?.rename(indexPath: indexPath) completion(true) } @@ -354,7 +354,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner { return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) } else if feed is Folder { return makeFolderContextMenu(indexPath: indexPath) - } else if feed is PseudoFeed { + } else if feed is PseudoFeed { return makePseudoFeedContextMenu(indexPath: indexPath) } else { return nil @@ -686,7 +686,7 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate { return nil } - return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in + return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { _ in var menuElements = [UIMenuElement]() menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.getAccountInfoAction(account: account)])) @@ -890,7 +890,7 @@ private extension MainFeedViewController { } func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in + return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] _ in guard let self = self else { return nil } @@ -935,7 +935,7 @@ private extension MainFeedViewController { } func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in + return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] _ in guard let self = self else { return nil } @@ -962,7 +962,7 @@ private extension MainFeedViewController { return nil } - return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in + return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { _ in return UIMenu(title: "", children: [markAllAction]) }) } @@ -973,7 +973,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Open Home Page", comment: "Open Home Page") - let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] _ in self?.coordinator.showBrowserForFeed(indexPath) } return action @@ -985,7 +985,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Open Home Page", comment: "Open Home Page") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in self?.coordinator.showBrowserForFeed(indexPath) completion(true) } @@ -999,7 +999,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") - let action = UIAction(title: title, image: AppAssets.copyImage) { action in + let action = UIAction(title: title, image: AppAssets.copyImage) { _ in UIPasteboard.general.url = url } return action @@ -1012,7 +1012,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") - let action = UIAlertAction(title: title, style: .default) { action in + let action = UIAlertAction(title: title, style: .default) { _ in UIPasteboard.general.url = url completion(true) } @@ -1027,7 +1027,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") - let action = UIAction(title: title, image: AppAssets.copyImage) { action in + let action = UIAction(title: title, image: AppAssets.copyImage) { _ in UIPasteboard.general.url = url } return action @@ -1041,7 +1041,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") - let action = UIAlertAction(title: title, style: .default) { action in + let action = UIAlertAction(title: title, style: .default) { _ in UIPasteboard.general.url = url completion(true) } @@ -1061,8 +1061,7 @@ private extension MainFeedViewController { completion(true) } - - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAllAsRead(Array(articles)) completion(true) @@ -1074,7 +1073,7 @@ private extension MainFeedViewController { func deleteAction(indexPath: IndexPath) -> UIAction { let title = NSLocalizedString("Delete", comment: "Delete") - let action = UIAction(title: title, image: AppAssets.trashImage, attributes: .destructive) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.trashImage, attributes: .destructive) { [weak self] _ in self?.delete(indexPath: indexPath) } return action @@ -1082,7 +1081,7 @@ private extension MainFeedViewController { func renameAction(indexPath: IndexPath) -> UIAction { let title = NSLocalizedString("Rename", comment: "Rename") - let action = UIAction(title: title, image: AppAssets.editImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.editImage) { [weak self] _ in self?.rename(indexPath: indexPath) } return action @@ -1094,7 +1093,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Get Info", comment: "Get Info") - let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] _ in self?.coordinator.showFeedInspector(for: feed) } return action @@ -1102,7 +1101,7 @@ private extension MainFeedViewController { func getAccountInfoAction(account: Account) -> UIAction { let title = NSLocalizedString("Get Info", comment: "Get Info") - let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] _ in self?.coordinator.showAccountInspector(for: account) } return action @@ -1110,7 +1109,7 @@ private extension MainFeedViewController { func deactivateAccountAction(account: Account) -> UIAction { let title = NSLocalizedString("Deactivate", comment: "Deactivate") - let action = UIAction(title: title, image: AppAssets.deactivateImage) { action in + let action = UIAction(title: title, image: AppAssets.deactivateImage) { _ in account.isActive = false } return action @@ -1122,7 +1121,7 @@ private extension MainFeedViewController { } let title = NSLocalizedString("Get Info", comment: "Get Info") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in self?.coordinator.showFeedInspector(for: feed) completion(true) } @@ -1138,7 +1137,7 @@ private extension MainFeedViewController { let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String - let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in if let articles = try? feed.fetchUnreadArticles() { self?.coordinator.markAllAsRead(Array(articles)) @@ -1156,7 +1155,7 @@ private extension MainFeedViewController { let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String - let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in // If you don't have this delay the screen flashes when it executes this code DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { @@ -1170,7 +1169,6 @@ private extension MainFeedViewController { return action } - func rename(indexPath: IndexPath) { guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return } @@ -1183,7 +1181,7 @@ private extension MainFeedViewController { alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) let renameTitle = NSLocalizedString("Rename", comment: "Rename") - let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in + let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] _ in guard let name = alertController.textFields?[0].text, !name.isEmpty else { return @@ -1214,7 +1212,7 @@ private extension MainFeedViewController { alertController.addAction(renameAction) alertController.preferredAction = renameAction - alertController.addTextField() { textField in + alertController.addTextField { textField in textField.text = feed.nameForDisplay textField.placeholder = NSLocalizedString("Name", comment: "Name") } @@ -1234,7 +1232,7 @@ private extension MainFeedViewController { title = NSLocalizedString("Delete Folder", comment: "Delete folder") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text") message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String - } else { + } else { title = NSLocalizedString("Delete Feed", comment: "Delete feed") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text") message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String @@ -1246,7 +1244,7 @@ private extension MainFeedViewController { alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) let deleteTitle = NSLocalizedString("Delete", comment: "Delete") - let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in + let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] _ in self?.performDelete(indexPath: indexPath) } alertController.addAction(deleteAction) @@ -1284,6 +1282,6 @@ extension MainFeedViewController: UIGestureRecognizerDelegate { return false } let velocity = gestureRecognizer.velocity(in: self.view) - return abs(velocity.x) > abs(velocity.y); + return abs(velocity.x) > abs(velocity.y) } } diff --git a/iOS/MainFeed/RefreshProgressView.swift b/iOS/MainFeed/RefreshProgressView.swift index 937ab8345..3045c8168 100644 --- a/iOS/MainFeed/RefreshProgressView.swift +++ b/iOS/MainFeed/RefreshProgressView.swift @@ -10,10 +10,10 @@ import UIKit import Account class RefreshProgressView: UIView { - + @IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var label: UILabel! - + override func awakeFromNib() { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil) @@ -24,7 +24,7 @@ class RefreshProgressView: UIView { isAccessibilityElement = true accessibilityTraits = [.updatesFrequently, .notEnabled] } - + func update() { if !AccountManager.shared.combinedRefreshProgress.isComplete { progressChanged(animated: false) @@ -50,7 +50,7 @@ class RefreshProgressView: UIView { deinit { NotificationCenter.default.removeObserver(self) } - + } // MARK: Private @@ -68,7 +68,7 @@ private extension RefreshProgressView { if isInViewHierarchy { progressView.setProgress(1, animated: animated) } - + func completeLabel() { // Check that there are no pending downloads. if AccountManager.shared.combinedRefreshProgress.isComplete { @@ -101,7 +101,7 @@ private extension RefreshProgressView { } } } - + func updateRefreshLabel() { if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime { @@ -131,5 +131,5 @@ private extension RefreshProgressView { self?.scheduleUpdateRefreshLabel() } } - + } diff --git a/iOS/MainTimeline/Cell/MainTimelineAccessibilityCellLayout.swift b/iOS/MainTimeline/Cell/MainTimelineAccessibilityCellLayout.swift index c72345ae1..8786ad2b4 100644 --- a/iOS/MainTimeline/Cell/MainTimelineAccessibilityCellLayout.swift +++ b/iOS/MainTimeline/Cell/MainTimelineAccessibilityCellLayout.swift @@ -19,7 +19,7 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout { let summaryRect: CGRect let feedNameRect: CGRect let dateRect: CGRect - + init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) { var currentPoint = CGPoint.zero @@ -40,13 +40,13 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout { } else { self.iconImageRect = CGRect.zero } - + let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right) // Title Text Block let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth) self.titleRect = titleRect - + // Summary Text Block if self.titleRect != CGRect.zero { currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin @@ -54,21 +54,21 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout { self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle) currentPoint.y = [self.titleRect, self.summaryRect].maxY() - + if cellData.showFeedName != .none { self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth) currentPoint.y = self.feedNameRect.maxY } else { self.feedNameRect = CGRect.zero } - + // Feed Name and Pub Date self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth) self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom } - + } // MARK: - Calculate Rects @@ -78,13 +78,13 @@ private extension MainTimelineAccessibilityCellLayout { static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { var r = CGRect.zero - + let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont) r.size = size r.origin = point - + return r - + } - + } diff --git a/iOS/MainTimeline/Cell/MainTimelineCellData.swift b/iOS/MainTimeline/Cell/MainTimelineCellData.swift index 397f60409..da294e22d 100644 --- a/iOS/MainTimeline/Cell/MainTimelineCellData.swift +++ b/iOS/MainTimeline/Cell/MainTimelineCellData.swift @@ -12,7 +12,7 @@ import Articles struct MainTimelineCellData { private static let noText = NSLocalizedString("(No Text)", comment: "No Text") - + let title: String let attributedTitle: NSAttributedString let summary: String @@ -38,16 +38,15 @@ struct MainTimelineCellData { } else { self.summary = truncatedSummary } - + self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) if let feedName = feedName { self.feedName = ArticleStringFormatter.truncatedFeedName(feedName) - } - else { + } else { self.feedName = "" } - + if let byline = byline { self.byline = byline } else { @@ -63,10 +62,10 @@ struct MainTimelineCellData { self.starred = article.status.starred self.numberOfLines = numberOfLines self.iconSize = iconSize - + } - init() { //Empty + init() { // Empty self.title = "" self.attributedTitle = NSAttributedString() self.summary = "" @@ -81,5 +80,5 @@ struct MainTimelineCellData { self.numberOfLines = 0 self.iconSize = .medium } - + } diff --git a/iOS/MainTimeline/Cell/MainTimelineCellLayout.swift b/iOS/MainTimeline/Cell/MainTimelineCellLayout.swift index 5d8ba6b69..baf552abb 100644 --- a/iOS/MainTimeline/Cell/MainTimelineCellLayout.swift +++ b/iOS/MainTimeline/Cell/MainTimelineCellLayout.swift @@ -18,7 +18,7 @@ protocol MainTimelineCellLayout { var summaryRect: CGRect {get} var feedNameRect: CGRect {get} var dateRect: CGRect {get} - + } extension MainTimelineCellLayout { @@ -30,8 +30,7 @@ extension MainTimelineCellLayout { r.origin.y = point.y + 5 return r } - - + static func rectForStar(_ point: CGPoint) -> CGRect { var r = CGRect.zero r.size.width = MainTimelineDefaultCellLayout.starDimension @@ -40,7 +39,7 @@ extension MainTimelineCellLayout { r.origin.y = point.y + 3 return r } - + static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect { var r = CGRect.zero r.size = iconSize.size @@ -48,16 +47,16 @@ extension MainTimelineCellLayout { r.origin.y = point.y + 4 return r } - + static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) { var r = CGRect.zero if cellData.title.isEmpty { return (r, 0) } - + r.origin = point - + let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, width: Int(textAreaWidth)) r.size.width = textAreaWidth @@ -65,22 +64,22 @@ extension MainTimelineCellLayout { if sizeInfo.numberOfLinesUsed < 1 { r.size.height = 0 } - + return (r, sizeInfo.numberOfLinesUsed) - + } - + static func rectForSummary(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect { let linesLeft = cellData.numberOfLines - linesUsed - + var r = CGRect.zero if cellData.summary.isEmpty || linesLeft < 1 { return r } - + r.origin = point - + let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth)) r.size.width = textAreaWidth @@ -88,26 +87,26 @@ extension MainTimelineCellLayout { if sizeInfo.numberOfLinesUsed < 1 { r.size.height = 0 } - + return r - + } static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { var r = CGRect.zero r.origin = point - + let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont) r.size = size - + if r.size.width > textAreaWidth { r.size.width = textAreaWidth } - + return r - + } - + } diff --git a/iOS/MainTimeline/Cell/MainTimelineDefaultCellLayout.swift b/iOS/MainTimeline/Cell/MainTimelineDefaultCellLayout.swift index e85f43ef7..65b31f19c 100644 --- a/iOS/MainTimeline/Cell/MainTimelineDefaultCellLayout.swift +++ b/iOS/MainTimeline/Cell/MainTimelineDefaultCellLayout.swift @@ -12,7 +12,7 @@ import RSCore struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20) - + static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0) static let unreadCircleDimension = CGFloat(integerLiteral: 12) static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension) @@ -33,7 +33,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { return UIFont.preferredFont(forTextStyle: .footnote) } static let feedRightMargin = CGFloat(integerLiteral: 8) - + static var dateFont: UIFont { return UIFont.preferredFont(forTextStyle: .footnote) } @@ -72,13 +72,13 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { } else { self.iconImageRect = CGRect.zero } - + let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right) // Title Text Block let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth) self.titleRect = titleRect - + // Summary Text Block if self.titleRect != CGRect.zero { currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin @@ -93,7 +93,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { y -= tmp.height } currentPoint.y = y - + // Feed Name and Pub Date self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth) @@ -103,7 +103,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom } - + } // MARK: - Calculate Rects @@ -113,14 +113,14 @@ extension MainTimelineDefaultCellLayout { static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { var r = CGRect.zero - + let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont) r.size = size r.origin.x = (point.x + textAreaWidth) - size.width r.origin.y = point.y return r - + } - + } diff --git a/iOS/MainTimeline/Cell/MainTimelineTableViewCell.swift b/iOS/MainTimeline/Cell/MainTimelineTableViewCell.swift index f508029b0..d32ca7b63 100644 --- a/iOS/MainTimeline/Cell/MainTimelineTableViewCell.swift +++ b/iOS/MainTimeline/Cell/MainTimelineTableViewCell.swift @@ -18,11 +18,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell { private let feedNameView = MainTimelineTableViewCell.singleLineUILabel() private lazy var iconView = IconView() - + private lazy var starView = { return NonIntrinsicImageView(image: AppAssets.timelineStarImage) }() - + private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator? private var starViewPropertyAnimator: UIViewPropertyAnimator? @@ -31,12 +31,12 @@ class MainTimelineTableViewCell: VibrantTableViewCell { updateSubviews() } } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + override func prepareForReuse() { unreadIndicatorPropertyAnimator?.stopAnimation(true) unreadIndicatorPropertyAnimator = nil @@ -46,19 +46,19 @@ class MainTimelineTableViewCell: VibrantTableViewCell { starViewPropertyAnimator = nil starView.isHidden = true } - + override var frame: CGRect { didSet { setNeedsLayout() } } - + override func updateVibrancy(animated: Bool) { updateLabelVibrancy(titleView, color: labelColor, animated: animated) updateLabelVibrancy(summaryView, color: labelColor, animated: animated) updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated) updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated) - + if animated { UIView.animate(withDuration: Self.duration) { if self.isHighlighted || self.isSelected { @@ -75,16 +75,16 @@ class MainTimelineTableViewCell: VibrantTableViewCell { } } } - + override func sizeThatFits(_ size: CGSize) -> CGSize { let layout = updatedLayout(width: size.width) return CGSize(width: size.width, height: layout.height) } override func layoutSubviews() { - + super.layoutSubviews() - + let layout = updatedLayout(width: bounds.width) unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect) @@ -97,11 +97,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell { separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } - + func setIconImage(_ image: IconImage) { iconView.iconImage = image } - + } // MARK: - Private @@ -115,7 +115,7 @@ private extension MainTimelineTableViewCell { label.adjustsFontForContentSizeCategory = true return label } - + static func multiLineUILabel() -> UILabel { let label = NonIntrinsicLabel() label.numberOfLines = 0 @@ -124,16 +124,16 @@ private extension MainTimelineTableViewCell { label.adjustsFontForContentSizeCategory = true return label } - + func setFrame(for label: UILabel, rect: CGRect) { - + if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 { hideView(label) } else { showView(label) label.setFrameIfNotEqual(rect) } - + } func addSubviewAtInit(_ view: UIView, hidden: Bool) { @@ -141,9 +141,9 @@ private extension MainTimelineTableViewCell { view.translatesAutoresizingMaskIntoConstraints = false view.isHidden = hidden } - + func commonInit() { - + addSubviewAtInit(titleView, hidden: false) addSubviewAtInit(summaryView, hidden: true) addSubviewAtInit(unreadIndicatorView, hidden: true) @@ -152,7 +152,7 @@ private extension MainTimelineTableViewCell { addSubviewAtInit(iconView, hidden: true) addSubviewAtInit(starView, hidden: true) } - + func updatedLayout(width: CGFloat) -> MainTimelineCellLayout { if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory { return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData) @@ -160,25 +160,25 @@ private extension MainTimelineTableViewCell { return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData) } } - + func updateTitleView() { titleView.font = MainTimelineDefaultCellLayout.titleFont titleView.textColor = labelColor updateTextFieldAttributedText(titleView, cellData?.attributedTitle) } - + func updateSummaryView() { summaryView.font = MainTimelineDefaultCellLayout.summaryFont summaryView.textColor = labelColor updateTextFieldText(summaryView, cellData?.summary) } - + func updateDateView() { dateView.font = MainTimelineDefaultCellLayout.dateFont dateView.textColor = secondaryLabelColor updateTextFieldText(dateView, cellData.dateString) } - + func updateTextFieldText(_ label: UILabel, _ text: String?) { let s = text ?? "" if label.text != s { @@ -199,7 +199,7 @@ private extension MainTimelineTableViewCell { setNeedsLayout() } } - + func updateFeedNameView() { switch cellData.showFeedName { case .feed: @@ -216,7 +216,7 @@ private extension MainTimelineTableViewCell { hideView(feedNameView) } } - + func updateUnreadIndicator() { if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred { unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in @@ -233,7 +233,7 @@ private extension MainTimelineTableViewCell { showOrHideView(unreadIndicatorView, cellData.read || cellData.starred) } } - + func updateStarView() { if !starView.isHidden && cellData.read && !cellData.starred { starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in @@ -250,7 +250,7 @@ private extension MainTimelineTableViewCell { showOrHideView(starView, !cellData.starred) } } - + func updateIconImage() { guard let image = cellData.iconImage, cellData.showIcon else { makeIconEmpty() @@ -258,20 +258,20 @@ private extension MainTimelineTableViewCell { } showView(iconView) - + if iconView.iconImage !== cellData.iconImage { iconView.iconImage = image setNeedsLayout() } } - + func updateAccessiblityLabel() { let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : "" let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), " let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)" accessibilityLabel = label } - + func makeIconEmpty() { if iconView.iconImage != nil { iconView.iconImage = nil @@ -279,23 +279,23 @@ private extension MainTimelineTableViewCell { } hideView(iconView) } - + func hideView(_ view: UIView) { if !view.isHidden { view.isHidden = true } } - + func showView(_ view: UIView) { if view.isHidden { view.isHidden = false } } - + func showOrHideView(_ view: UIView, _ shouldHide: Bool) { shouldHide ? hideView(view) : showView(view) } - + func updateSubviews() { updateTitleView() updateSummaryView() @@ -306,5 +306,5 @@ private extension MainTimelineTableViewCell { updateIconImage() updateAccessiblityLabel() } - + } diff --git a/iOS/MainTimeline/Cell/MainUnreadIndicatorView.swift b/iOS/MainTimeline/Cell/MainUnreadIndicatorView.swift index d0473d95f..311ac196d 100644 --- a/iOS/MainTimeline/Cell/MainUnreadIndicatorView.swift +++ b/iOS/MainTimeline/Cell/MainUnreadIndicatorView.swift @@ -15,5 +15,5 @@ class MainUnreadIndicatorView: UIView { layer.cornerRadius = frame.size.width / 2.0 clipsToBounds = true } - + } diff --git a/iOS/MainTimeline/Cell/MultilineUILabelSizer.swift b/iOS/MainTimeline/Cell/MultilineUILabelSizer.swift index c5859ba42..9e1eb12ea 100644 --- a/iOS/MainTimeline/Cell/MultilineUILabelSizer.swift +++ b/iOS/MainTimeline/Cell/MultilineUILabelSizer.swift @@ -42,7 +42,7 @@ final class MultilineUILabelSizer { self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font) self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font) - + } static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo { @@ -52,7 +52,7 @@ final class MultilineUILabelSizer { static func emptyCache() { sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]() } - + } // MARK: - Private @@ -69,7 +69,7 @@ private extension MultilineUILabelSizer { let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font) sizers[specifier] = newSizer return newSizer - + } func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo { @@ -80,7 +80,7 @@ private extension MultilineUILabelSizer { let size = CGSize(width: width, height: textFieldHeight) let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed) return sizeInfo - + } func height(for string: String, width: Int) -> Int { @@ -98,14 +98,14 @@ private extension MultilineUILabelSizer { } var height = MultilineUILabelSizer.calculateHeight(string, width, font) - + if numberOfLines != 0 { let maxHeight = singleLineHeightEstimate * numberOfLines if height > maxHeight { height = maxHeight } } - + cache[string]![width] = height return height @@ -123,7 +123,7 @@ private extension MultilineUILabelSizer { let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0 let lines = Int(round(CGFloat(height) / averageHeight)) return lines - + } func heightIsProbablySingleLineHeight(_ height: Int) -> Bool { @@ -140,7 +140,7 @@ private extension MultilineUILabelSizer { let minimum = estimate - slop let maximum = estimate + slop return height >= minimum && height <= maximum - + } func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? { @@ -165,8 +165,7 @@ private extension MultilineUILabelSizer { if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) { smallNeighbor = (oneWidth, oneHeight) - } - else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) { + } else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) { largeNeighbor = (oneWidth, oneHeight) } @@ -176,7 +175,7 @@ private extension MultilineUILabelSizer { } return nil - + } - + } diff --git a/iOS/MainTimeline/Cell/SingleLineUILabelSizer.swift b/iOS/MainTimeline/Cell/SingleLineUILabelSizer.swift index 901df9a77..a37d33806 100644 --- a/iOS/MainTimeline/Cell/SingleLineUILabelSizer.swift +++ b/iOS/MainTimeline/Cell/SingleLineUILabelSizer.swift @@ -30,10 +30,10 @@ final class SingleLineUILabelSizer { let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font) let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font) let calculatedSize = CGSize(width: ceil(width), height: ceil(height)) - + cache[text] = calculatedSize return calculatedSize - + } static private var sizers = [UIFont: SingleLineUILabelSizer]() @@ -48,7 +48,7 @@ final class SingleLineUILabelSizer { sizers[font] = newSizer return newSizer - + } // Use this call. It’s easiest. @@ -60,5 +60,5 @@ final class SingleLineUILabelSizer { static func emptyCache() { sizers = [UIFont: SingleLineUILabelSizer]() } - + } diff --git a/iOS/MainTimeline/MainTimelineDataSource.swift b/iOS/MainTimeline/MainTimelineDataSource.swift index f5c6d2b85..c959aaec1 100644 --- a/iOS/MainTimeline/MainTimelineDataSource.swift +++ b/iOS/MainTimeline/MainTimelineDataSource.swift @@ -8,11 +8,10 @@ import UIKit -class MainTimelineDataSource: UITableViewDiffableDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { +class MainTimelineDataSource: UITableViewDiffableDataSource where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } - -} +} diff --git a/iOS/MainTimeline/MainTimelineTitleView.swift b/iOS/MainTimeline/MainTimelineTitleView.swift index dc9ed363a..7099ec881 100644 --- a/iOS/MainTimeline/MainTimelineTitleView.swift +++ b/iOS/MainTimeline/MainTimelineTitleView.swift @@ -24,8 +24,7 @@ class MainTimelineTitleView: UIView { if let name = label.text { let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility") return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)" - } - else { + } else { return nil } } @@ -36,7 +35,7 @@ class MainTimelineTitleView: UIView { accessibilityTraits = .button addInteraction(pointerInteraction) } - + func debuttonize() { heightAnchor.constraint(equalToConstant: 40.0).isActive = true accessibilityTraits.remove(.button) @@ -45,7 +44,7 @@ class MainTimelineTitleView: UIView { } extension MainTimelineTitleView: UIPointerInteractionDelegate { - + func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { var rect = self.frame rect.origin.x = rect.origin.x - 10 diff --git a/iOS/MainTimeline/MainTimelineUnreadCountView.swift b/iOS/MainTimeline/MainTimelineUnreadCountView.swift index 6a0f99e3b..6a8d1dca1 100644 --- a/iOS/MainTimeline/MainTimelineUnreadCountView.swift +++ b/iOS/MainTimeline/MainTimelineUnreadCountView.swift @@ -17,11 +17,11 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView { override var textColor: UIColor { return UIColor.systemBackground } - + override var intrinsicContentSize: CGSize { return contentSize } - + override func draw(_ dirtyRect: CGRect) { let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius) @@ -33,7 +33,7 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView { if unreadCount > 0 { unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes) } - + } - + } diff --git a/iOS/MainTimeline/MarkAsReadAlertController.swift b/iOS/MainTimeline/MarkAsReadAlertController.swift index 5bbce1406..92f334860 100644 --- a/iOS/MainTimeline/MarkAsReadAlertController.swift +++ b/iOS/MainTimeline/MarkAsReadAlertController.swift @@ -14,21 +14,20 @@ extension CGRect: MarkAsReadAlertControllerSourceType {} extension UIView: MarkAsReadAlertControllerSourceType {} extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {} - struct MarkAsReadAlertController { - + static func confirm(_ controller: UIViewController?, coordinator: SceneCoordinator?, confirmTitle: String, sourceType: T, cancelCompletion: (() -> Void)? = nil, completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType { - + guard let controller = controller, let coordinator = coordinator else { completion() return } - + if AppDefaults.shared.confirmMarkAllAsRead { let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in completion() @@ -38,20 +37,19 @@ struct MarkAsReadAlertController { completion() } } - + private static func alert(coordinator: SceneCoordinator, confirmTitle: String, cancelCompletion: (() -> Void)?, sourceType: T, - completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { - - + completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { + let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") let message = NSLocalizedString("You can turn this confirmation off in Settings.", comment: "You can turn this confirmation off in Settings.") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") - + let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in cancelCompletion?() @@ -60,24 +58,24 @@ struct MarkAsReadAlertController { coordinator.showSettings(scrollToArticlesSection: true) } let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion) - + alertController.addAction(markAction) alertController.addAction(settingsAction) alertController.addAction(cancelAction) - + if let barButtonItem = sourceType as? UIBarButtonItem { alertController.popoverPresentationController?.barButtonItem = barButtonItem } - + if let rect = sourceType as? CGRect { alertController.popoverPresentationController?.sourceRect = rect } - + if let view = sourceType as? UIView { alertController.popoverPresentationController?.sourceView = view } return alertController } - + } diff --git a/iOS/MainTimeline/TimelineViewController.swift b/iOS/MainTimeline/TimelineViewController.swift index 226e10d5a..f75f9bcd0 100644 --- a/iOS/MainTimeline/TimelineViewController.swift +++ b/iOS/MainTimeline/TimelineViewController.swift @@ -17,8 +17,8 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { private var numberOfTextLines = 0 private var iconSize = IconSize.medium - private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) - + private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showFeedInspector(_:))) + private var refreshProgressView: RefreshProgressView? @IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @@ -28,28 +28,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { private lazy var dataSource = makeDataSource() private let searchController = UISearchController(searchResultsController: nil) - + weak var coordinator: SceneCoordinator! var undoableCommands = [UndoableCommand]() let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { - + // If the first responder is the WKWebView we don't want to supply any keyboard // commands that the system is looking for by going up the responder chain. They will interfere with // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } - + return keyboardManager.keyCommands } - + override var canBecomeFirstResponder: Bool { return true } override func viewDidLoad() { - + super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) @@ -68,11 +68,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) - + // Initialize Programmatic Buttons filterButton = UIBarButtonItem(image: AppAssets.filterInactiveImage, style: .plain, target: self, action: #selector(toggleFilter(_:))) firstUnreadButton = UIBarButtonItem(image: AppAssets.nextUnreadArticleImage, style: .plain, target: self, action: #selector(firstUnread(_:))) - + // Setup the Search Controller searchController.delegate = self searchController.searchResultsUpdater = self @@ -97,27 +97,27 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { if let titleView = Bundle.main.loadNibNamed("MainTimelineTitleView", owner: self, options: nil)?[0] as? MainTimelineTitleView { navigationItem.titleView = titleView } - + refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) - + configureToolbar() resetUI(resetScroll: true) - + // Load the table and then scroll to the saved position if available applyChanges(animated: false) { if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath { self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false) } } - + // Disable swipe back on iPad Mice guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else { return } gesture.allowedScrollTypesMask = [] } - + override func viewWillAppear(_ animated: Bool) { self.navigationController?.isToolbarHidden = false @@ -125,10 +125,10 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { if navigationController?.navigationBar.isHidden ?? false { navigationController?.navigationBar.alpha = 0 } - + super.viewWillAppear(animated) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(true) coordinator.isTimelineViewControllerPending = false @@ -139,9 +139,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { } } } - + // MARK: Actions - + @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentArticle() } @@ -149,35 +149,35 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { @objc func openInAppBrowser(_ sender: Any?) { coordinator.showInAppBrowser() } - + @IBAction func toggleFilter(_ sender: Any) { coordinator.toggleReadArticlesFilter() } - + @IBAction func markAllAsRead(_ sender: Any) { let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") - + if let source = sender as? UIBarButtonItem { MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in self?.coordinator.markAllAsReadInTimeline() } } - + if let _ = sender as? UIKeyCommand { guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else { return } - + MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAllAsReadInTimeline() } } } - + @IBAction func firstUnread(_ sender: Any) { coordinator.selectFirstUnread() } - + @objc func refreshAccounts(_ sender: Any) { refreshControl?.endRefreshing() @@ -187,9 +187,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self)) } } - + // MARK: Keyboard shortcuts - + @objc func selectNextUp(_ sender: Any?) { coordinator.selectPrevArticle() } @@ -201,17 +201,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { @objc func navigateToSidebar(_ sender: Any?) { coordinator.navigateToFeeds() } - + @objc func navigateToDetail(_ sender: Any?) { coordinator.navigateToDetail() } - + @objc func showFeedInspector(_ sender: Any?) { coordinator.showFeedInspector() } // MARK: API - + func restoreSelectionIfNecessary(adjustScroll: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if adjustScroll { @@ -225,11 +225,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { func reinitializeArticles(resetScroll: Bool) { resetUI(resetScroll: resetScroll) } - + func reloadArticles(animated: Bool) { applyChanges(animated: animated) } - + func updateArticleSelection(animations: Animations) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if tableView.indexPathForSelectedRow != indexPath { @@ -238,7 +238,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { } else { tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none) } - + updateUI() } @@ -247,7 +247,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { updateTitleUnreadCount() updateToolbar() } - + func hideSearch() { navigationItem.searchController?.isActive = false } @@ -257,7 +257,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1 navigationItem.searchController?.searchBar.becomeFirstResponder() } - + func focus() { becomeFirstResponder() } @@ -265,7 +265,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { func setRefreshToolbarItemVisibility(visible: Bool) { refreshProgressView?.alpha = visible ? 1.0 : 0 } - + // MARK: - Table view override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { @@ -276,41 +276,41 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { let readTitle = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read") - - let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in + + let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (_, _, completion) in self?.coordinator.toggleRead(article) completion(true) } - + readAction.image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage readAction.backgroundColor = AppAssets.primaryAccentColor - + return UISwipeActionsConfiguration(actions: [readAction]) } - + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - + guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } - + // Set up the star action let starTitle = article.status.starred ? NSLocalizedString("Unstar", comment: "Unstar") : NSLocalizedString("Star", comment: "Star") - - let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (action, view, completion) in + + let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (_, _, completion) in self?.coordinator.toggleStar(article) completion(true) } - + starAction.image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage starAction.backgroundColor = AppAssets.starColor - + // Set up the read action let moreTitle = NSLocalizedString("More", comment: "More") let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in - + if let self = self { - + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) if let popoverController = alert.popoverPresentationController { popoverController.sourceView = view @@ -324,11 +324,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) { alert.addAction(action) } - + if let action = self.discloseFeedAlertAction(article, completion: completion) { alert.addAction(action) } - + if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) { alert.addAction(action) } @@ -347,28 +347,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { }) self.present(alert, animated: true) - + } - + } - + moreAction.image = AppAssets.moreImage moreAction.backgroundColor = UIColor.systemGray return UISwipeActionsConfiguration(actions: [starAction, moreAction]) - + } override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } - - return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in + + return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] _ in guard let self = self else { return nil } - + var menuElements = [UIMenuElement]() - + var markActions = [UIAction]() if let action = self.toggleArticleReadStatusAction(article) { markActions.append(action) @@ -381,7 +381,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { markActions.append(action) } menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions)) - + var secondaryActions = [UIAction]() if let action = self.discloseFeedAction(article) { secondaryActions.append(action) @@ -392,7 +392,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { if !secondaryActions.isEmpty { menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions)) } - + var copyActions = [UIAction]() if let action = self.copyArticleURLAction(article) { copyActions.append(action) @@ -403,19 +403,19 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { if !copyActions.isEmpty { menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions)) } - + if let action = self.openInBrowserAction(article) { menuElements.append(UIMenu(title: "", options: .displayInline, children: [action])) } - + if let action = self.shareAction(article, indexPath: indexPath) { menuElements.append(UIMenu(title: "", options: .displayInline, children: [action])) } - + return UIMenu(title: "", children: menuElements) }) - + } override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { @@ -423,7 +423,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else { return nil } - + return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) } @@ -432,17 +432,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { let article = dataSource.itemIdentifier(for: indexPath) coordinator.selectArticle(article, animations: [.scroll, .select, .navigation]) } - + override func scrollViewDidScroll(_ scrollView: UIScrollView) { scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) } - + // MARK: Notifications @objc dynamic func unreadCountDidChange(_ notification: Notification) { updateUI() } - + @objc func statusesDidChange(_ note: Notification) { guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set, !articleIDs.isEmpty else { return @@ -461,11 +461,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { } @objc func feedIconDidBecomeAvailable(_ note: Notification) { - + if let titleView = navigationItem.titleView as? MainTimelineTitleView { titleView.iconView.iconImage = coordinator.timelineIconImage } - + guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { return } @@ -519,27 +519,27 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { self.updateToolbar() } } - + @objc func contentSizeCategoryDidChange(_ note: Notification) { reloadAllVisibleCells() } - + @objc func displayNameDidChange(_ note: Notification) { if let titleView = navigationItem.titleView as? MainTimelineTitleView { titleView.label.text = coordinator.timelineFeed?.nameForDisplay } } - + @objc func willEnterForeground(_ note: Notification) { updateUI() } - + @objc func scrollPositionDidChange() { coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() } - + // MARK: Reloading - + func queueReloadAvailableCells() { CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells)) } @@ -566,7 +566,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: Constants.prototypeText, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, numberOfLines: numberOfTextLines, iconSize: iconSize) - + if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory { let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData) tableView.estimatedRowHeight = layout.height @@ -574,9 +574,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner { let layout = MainTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData) tableView.estimatedRowHeight = layout.height } - + } - + } // MARK: Searching @@ -619,7 +619,7 @@ private extension TimelineViewController { guard !(splitViewController?.isCollapsed ?? true) else { return } - + guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else { return } @@ -630,7 +630,7 @@ private extension TimelineViewController { } func resetUI(resetScroll: Bool) { - + title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline" if let titleView = navigationItem.titleView as? MainTimelineTitleView { @@ -641,7 +641,7 @@ private extension TimelineViewController { } else { titleView.iconView.tintColor = nil } - + titleView.label.text = coordinator.timelineFeed?.nameForDisplay updateTitleUnreadCount() @@ -652,7 +652,7 @@ private extension TimelineViewController { titleView.debuttonize() titleView.removeGestureRecognizer(feedTapGestureRecognizer) } - + navigationItem.titleView = titleView } @@ -662,7 +662,7 @@ private extension TimelineViewController { case .alwaysRead: navigationItem.rightBarButtonItem = nil } - + if coordinator.isReadArticlesFiltered { filterButton?.image = AppAssets.filterActiveImage filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles") @@ -679,16 +679,16 @@ private extension TimelineViewController { tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } } - + updateToolbar() } - + func updateToolbar() { guard firstUnreadButton != nil else { return } - + markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable - + if coordinator.isRootSplitCollapsed { if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton { var items = toolbarItems @@ -702,20 +702,20 @@ private extension TimelineViewController { } } } - + func updateTitleUnreadCount() { if let titleView = navigationItem.titleView as? MainTimelineTitleView { titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount } } - + func applyChanges(animated: Bool, completion: (() -> Void)? = nil) { if coordinator.articles.count == 0 { tableView.rowHeight = tableView.estimatedRowHeight } else { tableView.rowHeight = UITableView.automaticDimension } - + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([0]) snapshot.appendItems(coordinator.articles, toSection: 0) @@ -725,7 +725,7 @@ private extension TimelineViewController { completion?() } } - + func makeDataSource() -> UITableViewDiffableDataSource { let dataSource: UITableViewDiffableDataSource = MainTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in @@ -736,7 +736,7 @@ private extension TimelineViewController { dataSource.defaultRowAnimation = .middle return dataSource } - + func configure(_ cell: MainTimelineTableViewCell, article: Article) { let iconImage = iconImageFor(article) @@ -746,29 +746,29 @@ private extension TimelineViewController { cell.cellData = MainTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcon, numberOfLines: numberOfTextLines, iconSize: iconSize) } - + func iconImageFor(_ article: Article) -> IconImage? { if !coordinator.showIcons { return nil } return article.iconImage() } - + func toggleArticleReadStatusAction(_ article: Article) -> UIAction? { guard !article.status.read || article.isAvailableToMarkUnread else { return nil } - + let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read") let image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage - let action = UIAction(title: title, image: image) { [weak self] action in + let action = UIAction(title: title, image: image) { [weak self] _ in self?.coordinator.toggleRead(article) } - + return action } - + func toggleArticleStarStatusAction(_ article: Article) -> UIAction { let title = article.status.starred ? @@ -776,10 +776,10 @@ private extension TimelineViewController { NSLocalizedString("Mark as Starred", comment: "Mark as Starred") let image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage - let action = UIAction(title: title, image: image) { [weak self] action in + let action = UIAction(title: title, image: image) { [weak self] _ in self?.coordinator.toggleStar(article) } - + return action } @@ -790,14 +790,14 @@ private extension TimelineViewController { let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read") let image = AppAssets.markAboveAsReadImage - let action = UIAction(title: title, image: image) { [weak self] action in + let action = UIAction(title: title, image: image) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAboveAsRead(article) } } return action } - + func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil @@ -805,14 +805,14 @@ private extension TimelineViewController { let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read") let image = AppAssets.markBelowAsReadImage - let action = UIAction(title: title, image: image) { [weak self] action in + let action = UIAction(title: title, image: image) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markBelowAsRead(article) } } return action } - + func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil @@ -823,7 +823,7 @@ private extension TimelineViewController { completion(true) } - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAboveAsRead(article) completion(true) @@ -841,8 +841,8 @@ private extension TimelineViewController { let cancel = { completion(true) } - - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markBelowAsRead(article) completion(true) @@ -854,26 +854,26 @@ private extension TimelineViewController { func discloseFeedAction(_ article: Article) -> UIAction? { guard let feed = article.feed, !coordinator.timelineFeedIsEqualTo(feed) else { return nil } - + let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") - let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] _ in self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation]) } return action } - + func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard let feed = article.feed, !coordinator.timelineFeedIsEqualTo(feed) else { return nil } let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation]) completion(true) } return action } - + func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { guard let feed = article.feed else { return nil } guard let fetchedArticles = try? feed.fetchArticles() else { @@ -884,12 +884,11 @@ private extension TimelineViewController { guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } - - + let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String - - let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in + + let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in self?.coordinator.markAllAsRead(articles) } @@ -902,19 +901,19 @@ private extension TimelineViewController { guard let fetchedArticles = try? feed.fetchArticles() else { return nil } - + let articles = Array(fetchedArticles) guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } - + let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let cancel = { completion(true) } - - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in self?.coordinator.markAllAsRead(articles) completion(true) @@ -922,30 +921,29 @@ private extension TimelineViewController { } return action } - + func copyArticleURLAction(_ article: Article) -> UIAction? { guard let url = article.preferredURL else { return nil } let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL") - let action = UIAction(title: title, image: AppAssets.copyImage) { action in - UIPasteboard.general.url = url - } - return action - } - - func copyExternalURLAction(_ article: Article) -> UIAction? { - guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil } - let title = NSLocalizedString("Copy External URL", comment: "Copy External URL") - let action = UIAction(title: title, image: AppAssets.copyImage) { action in + let action = UIAction(title: title, image: AppAssets.copyImage) { _ in UIPasteboard.general.url = url } return action } + func copyExternalURLAction(_ article: Article) -> UIAction? { + guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil } + let title = NSLocalizedString("Copy External URL", comment: "Copy External URL") + let action = UIAction(title: title, image: AppAssets.copyImage) { _ in + UIPasteboard.general.url = url + } + return action + } func openInBrowserAction(_ article: Article) -> UIAction? { guard let _ = article.preferredURL else { return nil } let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") - let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] _ in self?.coordinator.showBrowserForArticle(article) } return action @@ -955,41 +953,41 @@ private extension TimelineViewController { guard let _ = article.preferredURL else { return nil } let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in self?.coordinator.showBrowserForArticle(article) completion(true) } return action } - + func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) { let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil) - + guard let cell = tableView.cellForRow(at: indexPath) else { return } let popoverController = activityViewController.popoverPresentationController popoverController?.sourceView = cell popoverController?.sourceRect = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height) - + present(activityViewController, animated: true) } - + func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? { guard let url = article.preferredURL else { return nil } let title = NSLocalizedString("Share", comment: "Share") - let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in + let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] _ in self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title) } return action } - + func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard let url = article.preferredURL else { return nil } let title = NSLocalizedString("Share", comment: "Share") - let action = UIAlertAction(title: title, style: .default) { [weak self] action in + let action = UIAlertAction(title: title, style: .default) { [weak self] _ in completion(true) self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title) } return action } - + } diff --git a/iOS/RootSplitViewController.swift b/iOS/RootSplitViewController.swift index cc7255e2a..253969c90 100644 --- a/iOS/RootSplitViewController.swift +++ b/iOS/RootSplitViewController.swift @@ -10,28 +10,28 @@ import UIKit import Account class RootSplitViewController: UISplitViewController { - + var coordinator: SceneCoordinator! - + override var prefersStatusBarHidden: Bool { return coordinator.prefersStatusBarHidden } - + override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { return .slide } - + override func viewDidAppear(_ animated: Bool) { coordinator.resetFocus() } - + override func show(_ column: UISplitViewController.Column) { guard !coordinator.isNavigationDisabled else { return } super.show(column) } - + // MARK: Keyboard Shortcuts - + @objc func scrollOrGoToNextUnread(_ sender: Any?) { coordinator.scrollOrGoToNextUnread() } @@ -39,26 +39,26 @@ class RootSplitViewController: UISplitViewController { @objc func scrollUp(_ sender: Any?) { coordinator.scrollUp() } - + @objc func goToPreviousUnread(_ sender: Any?) { coordinator.selectPrevUnread() } - + @objc func nextUnread(_ sender: Any?) { coordinator.selectNextUnread() } - + @objc func markRead(_ sender: Any?) { coordinator.markAsReadForCurrentArticle() } - + @objc func markUnreadAndGoToNextUnread(_ sender: Any?) { coordinator.markAsUnreadForCurrentArticle() coordinator.selectNextUnread() } - + @objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) { - coordinator.markAllAsReadInTimeline() { + coordinator.markAllAsReadInTimeline { self.coordinator.selectNextUnread() } } @@ -66,23 +66,23 @@ class RootSplitViewController: UISplitViewController { @objc func markAboveAsRead(_ sender: Any?) { coordinator.markAboveAsRead() } - + @objc func markBelowAsRead(_ sender: Any?) { coordinator.markBelowAsRead() } - + @objc func markUnread(_ sender: Any?) { coordinator.markAsUnreadForCurrentArticle() } - + @objc func goToPreviousSubscription(_ sender: Any?) { coordinator.selectPrevFeed() } - + @objc func goToNextSubscription(_ sender: Any?) { coordinator.selectNextFeed() } - + @objc func openInBrowser(_ sender: Any?) { coordinator.showBrowserForCurrentArticle() } @@ -90,11 +90,11 @@ class RootSplitViewController: UISplitViewController { @objc func openInAppBrowser(_ sender: Any?) { coordinator.showInAppBrowser() } - + @objc func articleSearch(_ sender: Any?) { coordinator.showSearch() } - + @objc func addNewFeed(_ sender: Any?) { coordinator.showAddFeed() } @@ -106,27 +106,27 @@ class RootSplitViewController: UISplitViewController { @objc func cleanUp(_ sender: Any?) { coordinator.cleanUp(conditional: false) } - + @objc func toggleReadFeedsFilter(_ sender: Any?) { coordinator.toggleReadFeedsFilter() } - + @objc func toggleReadArticlesFilter(_ sender: Any?) { coordinator.toggleReadArticlesFilter() } - + @objc func refresh(_ sender: Any?) { appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self)) } - + @objc func goToToday(_ sender: Any?) { coordinator.selectTodayFeed() } - + @objc func goToAllUnread(_ sender: Any?) { coordinator.selectAllUnreadFeed() } - + @objc func goToStarred(_ sender: Any?) { coordinator.selectStarredFeed() } @@ -138,7 +138,7 @@ class RootSplitViewController: UISplitViewController { @objc func toggleRead(_ sender: Any?) { coordinator.toggleReadForCurrentArticle() } - + @objc func toggleStarred(_ sender: Any?) { coordinator.toggleStarredForCurrentArticle() } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index d6a970ff9..86e994d09 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -28,32 +28,32 @@ enum ShowFeedName { struct FeedNode: Hashable { var node: Node var feedID: SidebarItemIdentifier - + init(_ node: Node) { self.node = node self.feedID = (node.representedObject as! SidebarItem).sidebarItemID! } - + func hash(into hasher: inout Hasher) { hasher.combine(feedID) } } class SceneCoordinator: NSObject, UndoableCommandRunner { - + var undoableCommands = [UndoableCommand]() var undoManager: UndoManager? { return rootSplitViewController.undoManager } - + private var activityManager = ActivityManager() - + private var rootSplitViewController: RootSplitViewController! private var mainFeedViewController: MainFeedViewController! private var mainTimelineViewController: TimelineViewController? private var articleViewController: ArticleViewController? - + private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) private let rebuildBackingStoresQueue = CoalescingQueue(name: "Rebuild The Backing Stores", interval: 0.5) private var fetchSerialNumber = 0 @@ -73,14 +73,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { private(set) var preSearchTimelineFeed: SidebarItem? private var lastSearchString = "" - private var lastSearchScope: SearchScope? = nil + private var lastSearchScope: SearchScope? private var isSearching: Bool = false - private var savedSearchArticles: ArticleArray? = nil - private var savedSearchArticleIds: Set? = nil - + private var savedSearchArticles: ArticleArray? + private var savedSearchArticleIds: Set? + var isTimelineViewControllerPending = false var isArticleViewControllerPending = false - + private(set) var sortDirection = AppDefaults.shared.timelineSortDirection { didSet { if sortDirection != oldValue { @@ -88,7 +88,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } } } - + private(set) var groupByFeed = AppDefaults.shared.timelineGroupByFeed { didSet { if groupByFeed != oldValue { @@ -96,18 +96,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } } } - + var prefersStatusBarHidden = false - + private let treeControllerDelegate = FeedTreeControllerDelegate() private let treeController: TreeController - + var stateRestorationActivity: NSUserActivity { let activity = activityManager.stateRestorationActivity var userInfo = activity.userInfo ?? [AnyHashable: Any]() - + userInfo[UserInfoKey.windowState] = windowState() - + let articleState = articleViewController?.currentState userInfo[UserInfoKey.isShowingExtractedArticle] = articleState?.isShowingExtractedArticle ?? false userInfo[UserInfoKey.articleWindowScrollY] = articleState?.windowScrollY ?? 0 @@ -115,17 +115,17 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { activity.userInfo = userInfo return activity } - + var isNavigationDisabled = false - + var isRootSplitCollapsed: Bool { return rootSplitViewController.isCollapsed } - + var isReadFeedsFiltered: Bool { return treeControllerDelegate.isReadFiltered } - + var isReadArticlesFiltered: Bool { if let feedID = timelineFeed?.sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] { return readFilterEnabled @@ -133,15 +133,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { return timelineDefaultReadFilterType != .none } } - + var timelineDefaultReadFilterType: ReadFilterType { return timelineFeed?.defaultReadFilterType ?? .none } - + var rootNode: Node { return treeController.rootNode } - + // At some point we should refactor the current Feed IndexPath out and only use the timeline feed private(set) var currentFeedIndexPath: IndexPath? @@ -151,12 +151,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } return IconImageCache.shared.imageForFeed(timelineFeed) } - + private var exceptionArticleFetcher: ArticleFetcher? private(set) var timelineFeed: SidebarItem? - + var timelineMiddleIndexPath: IndexPath? - + private(set) var showFeedNames = ShowFeedName.none private(set) var showIcons = false @@ -164,7 +164,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { guard let indexPath = currentFeedIndexPath else { return nil } - + let prevIndexPath: IndexPath? = { if indexPath.row - 1 < 0 { for i in (0..= shadowTable[indexPath.section].feedNodes.count { for i in indexPath.section + 1.. 0 } - + var isNextArticleAvailable: Bool { guard let articleRow = currentArticleRow else { return false } return articleRow + 1 < articles.count } - + var prevArticle: Article? { guard isPrevArticleAvailable, let articleRow = currentArticleRow else { return nil } return articles[articleRow - 1] } - + var nextArticle: Article? { guard isNextArticleAvailable, let articleRow = currentArticleRow else { return nil } return articles[articleRow + 1] } - + var firstUnreadArticleIndexPath: IndexPath? { for (row, article) in articles.enumerated() { if !article.status.read { @@ -238,7 +238,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } return nil } - + var currentArticle: Article? private(set) var articles = ArticleArray() { @@ -265,13 +265,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { var isTimelineUnreadAvailable: Bool { return timelineUnreadCount > 0 } - + var isAnyUnreadAvailable: Bool { return appDelegate.unreadCount > 0 } - + var timelineUnreadCount: Int = 0 - + init(rootSplitViewController: RootSplitViewController) { self.rootSplitViewController = rootSplitViewController self.treeController = TreeController(delegate: treeControllerDelegate) @@ -294,7 +294,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { markExpanded(sectionNode) shadowTable.append((sectionID: "", feedNodes: [FeedNode]())) } - + NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) @@ -311,15 +311,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(themeDownloadDidFail(_:)), name: .didFailToImportThemeWithError, object: nil) } - + func restoreWindowState(_ activity: NSUserActivity?) { if let activity = activity, let windowState = activity.userInfo?[UserInfoKey.windowState] as? [AnyHashable: Any] { - + if let containerExpandedWindowState = windowState[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] { let containerIdentifiers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) }) expandedTable = Set(containerIdentifiers) } - + if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] { for key in readArticlesFilterState.keys { if let feedIdentifier = SidebarItemIdentifier(userInfo: key) { @@ -335,14 +335,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { if let readFeedsFilterState = windowState[UserInfoKey.readFeedsFilterState] as? Bool { treeControllerDelegate.isReadFiltered = readFeedsFilterState } - + } else { - + rebuildBackingStores(initialLoad: true) - + } } - + func handle(_ activity: NSUserActivity) { selectFeed(indexPath: nil) { guard let activityType = ActivityType(rawValue: activity.activityType) else { return } @@ -360,12 +360,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } } } - + func handle(_ response: UNNotificationResponse) { let userInfo = response.notification.request.content.userInfo handleReadArticle(userInfo) } - + func resetFocus() { if currentArticle != nil { mainTimelineViewController?.focus() @@ -373,7 +373,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { mainFeedViewController?.focus() } } - + func selectFirstUnreadInAllUnread() { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.unreadFeed) { @@ -391,14 +391,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } } } - + // MARK: Notifications - + @objc func unreadCountDidInitialize(_ notification: Notification) { guard notification.object is AccountManager else { return } - + if isReadFeedsFiltered { rebuildBackingStores() } @@ -407,16 +407,16 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { @objc func unreadCountDidChange(_ note: Notification) { // We will handle the filtering of unread feeds in unreadCountDidInitialize after they have all be calculated guard AccountManager.shared.isUnreadCountsInitialized else { - return + return } - + queueRebuildBackingStores() } @objc func statusesDidChange(_ note: Notification) { updateUnreadCount() } - + @objc func containerChildrenDidChange(_ note: Notification) { if timelineFetcherContainsAnyPseudoFeed() || timelineFetcherContainsAnyFolder() { fetchAndMergeArticlesAsync(animated: true) { @@ -427,11 +427,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { rebuildBackingStores() } } - + @objc func batchUpdateDidPerform(_ notification: Notification) { rebuildBackingStores() } - + @objc func displayNameDidChange(_ note: Notification) { rebuildBackingStores() } @@ -446,7 +446,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.rebuildBackingStores() } } - + @objc func userDidAddAccount(_ note: Notification) { let expandNewAccount = { if let account = note.userInfo?[Account.UserInfoKey.account] as? Account, @@ -454,7 +454,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.markExpanded(node) } } - + if timelineFetcherContainsAnyPseudoFeed() { fetchAndMergeArticlesAsync(animated: true) { self.mainTimelineViewController?.reinitializeArticles(resetScroll: false) @@ -472,7 +472,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.unmarkExpanded(node) } } - + if timelineFetcherContainsAnyPseudoFeed() { fetchAndMergeArticlesAsync(animated: true) { self.mainTimelineViewController?.reinitializeArticles(resetScroll: false) @@ -489,23 +489,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } discloseFeed(feed, animations: [.scroll, .navigation]) } - + @objc func userDefaultsDidChange(_ note: Notification) { self.sortDirection = AppDefaults.shared.timelineSortDirection self.groupByFeed = AppDefaults.shared.timelineGroupByFeed } - + @objc func accountDidDownloadArticles(_ note: Notification) { guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set else { return } - + let shouldFetchAndMergeArticles = timelineFetcherContainsAnyFeed(feeds) || timelineFetcherContainsAnyPseudoFeed() if shouldFetchAndMergeArticles { queueFetchAndMergeArticles() } } - + @objc func willEnterForeground(_ note: Notification) { // Don't interfere with any fetch requests that we may have initiated before the app was returned to the foreground. // For example if you select Next Unread from the Home Screen Quick actions, you can start a request before we are @@ -514,18 +514,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { queueFetchAndMergeArticles() } } - + @objc func importDownloadedTheme(_ note: Notification) { guard let userInfo = note.userInfo, let url = userInfo["url"] as? URL else { return } - + DispatchQueue.main.async { self.importTheme(filename: url.path) } } - + @objc func themeDownloadDidFail(_ note: Notification) { guard let userInfo = note.userInfo, let error = userInfo["error"] as? Error else { @@ -537,13 +537,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } // MARK: API - + func suspend() { fetchAndMergeArticlesQueue.performCallsImmediately() rebuildBackingStoresQueue.performCallsImmediately() fetchRequestQueue.cancelAllRequests() } - + func cleanUp(conditional: Bool) { if isReadFeedsFiltered { rebuildBackingStores() @@ -552,7 +552,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { refreshTimeline(resetScroll: false) } } - + func toggleReadFeedsFilter() { if isReadFeedsFiltered { treeControllerDelegate.isReadFiltered = false @@ -562,7 +562,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { rebuildBackingStores() mainFeedViewController?.updateUI() } - + func toggleReadArticlesFilter() { guard let feedID = timelineFeed?.sidebarItemID else { return @@ -573,7 +573,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } else { readFilterEnabledTable[feedID] = true } - + refreshTimeline(resetScroll: false) } @@ -586,15 +586,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } }) } - + func numberOfSections() -> Int { return shadowTable.count } - + func numberOfRows(in section: Int) -> Int { return shadowTable[section].feedNodes.count } - + func nodeFor(_ indexPath: IndexPath) -> Node? { guard indexPath.section > -1 && indexPath.row > -1 && @@ -613,18 +613,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } return nil } - + func articleFor(_ articleID: String) -> Article? { return idToArticleDictionary[articleID] } - + func cappedIndexPath(_ indexPath: IndexPath) -> IndexPath { guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else { return IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1) } return indexPath } - + func unreadCountFor(_ node: Node) -> Int { // The coordinator supplies the unread count for the currently selected feed if node.representedObject === timelineFeed as AnyObject { @@ -636,7 +636,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { assertionFailure("This method should only be called for nodes that have an UnreadCountProvider as the represented object.") return 0 } - + func refreshTimeline(resetScroll: Bool) { if let article = self.currentArticle, let account = article.account { exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID) @@ -645,25 +645,25 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.mainTimelineViewController?.reinitializeArticles(resetScroll: resetScroll) } } - + func isExpanded(_ containerID: ContainerIdentifier) -> Bool { return expandedTable.contains(containerID) } - + func isExpanded(_ containerIdentifiable: ContainerIdentifiable) -> Bool { if let containerID = containerIdentifiable.containerID { return isExpanded(containerID) } return false } - + func isExpanded(_ node: Node) -> Bool { if let containerIdentifiable = node.representedObject as? ContainerIdentifiable { return isExpanded(containerIdentifiable) } return false } - + func expand(_ containerID: ContainerIdentifier) { markExpanded(containerID) rebuildBackingStores() @@ -688,13 +688,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } rebuildBackingStores() } - + func collapse(_ containerID: ContainerIdentifier) { unmarkExpanded(containerID) rebuildBackingStores() clearTimelineIfNoLongerAvailable() } - + /// This is a special function that expects the caller to change the disclosure arrow state outside this function. /// Failure to do so will get the Sidebar into an invalid state. func collapse(_ node: Node) { @@ -714,14 +714,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { rebuildBackingStores() clearTimelineIfNoLongerAvailable() } - + func mainFeedIndexPathForCurrentTimeline() -> IndexPath? { guard let node = treeController.rootNode.descendantNodeRepresentingObject(timelineFeed as AnyObject) else { return nil } return indexPathFor(node) } - + func selectFeed(_ feed: SidebarItem?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { let indexPath: IndexPath? = { if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) { @@ -732,13 +732,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { }() selectFeed(indexPath: indexPath, animations: animations, deselectArticle: deselectArticle, completion: completion) } - + func selectFeed(indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { guard indexPath != currentFeedIndexPath else { completion?() return } - + currentFeedIndexPath = indexPath mainFeedViewController.updateFeedSelection(animations: animations) @@ -747,7 +747,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? SidebarItem { - + self.activityManager.selecting(feed: feed) self.rootSplitViewController.show(.supplementary) setTimelineFeed(feed, animated: false) { @@ -756,9 +756,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } completion?() } - + } else { - + setTimelineFeed(nil, animated: false) { if self.isReadFeedsFiltered { self.rebuildBackingStores() @@ -767,23 +767,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.rootSplitViewController.show(.primary) completion?() } - + } - + } - + func selectPrevFeed() { if let indexPath = prevFeedIndexPath { selectFeed(indexPath: indexPath, animations: [.navigation, .scroll]) } } - + func selectNextFeed() { if let indexPath = nextFeedIndexPath { selectFeed(indexPath: indexPath, animations: [.navigation, .scroll]) } } - + func selectTodayFeed(completion: (() -> Void)? = nil) { markExpanded(SmartFeedsController.shared) self.ensureFeedIsAvailableToSelect(SmartFeedsController.shared.todayFeed) { @@ -807,18 +807,18 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { func selectArticle(_ article: Article?, animations: Animations = [], isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { guard article != currentArticle else { return } - + currentArticle = article activityManager.reading(feed: timelineFeed, article: article) - + if article == nil { rootSplitViewController.show(.supplementary) mainTimelineViewController?.updateArticleSelection(animations: animations) return } - + rootSplitViewController.show(.secondary) - + // Mark article as read before navigating to it, so the read status does not flash unread/read on display markArticles(Set([article!]), statusKey: .read, flag: true) @@ -828,7 +828,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { articleViewController?.restoreScrollPosition = (isShowingExtractedArticle, articleWindowScrollY) } } - + func beginSearching() { isSearching = true preSearchTimelineFeed = timelineFeed @@ -847,7 +847,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } else { setTimelineFeed(nil, animated: true) } - + lastSearchString = "" lastSearchScope = nil preSearchTimelineFeed = nil @@ -857,97 +857,97 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { selectArticle(nil) mainTimelineViewController?.focus() } - + func searchArticles(_ searchString: String, _ searchScope: SearchScope) { - + guard isSearching else { return } - + if searchString.count < 3 { setTimelineFeed(nil, animated: true) return } - + if searchString != lastSearchString || searchScope != lastSearchScope { - + switch searchScope { case .global: setTimelineFeed(SmartFeed(delegate: SearchFeedDelegate(searchString: searchString)), animated: true) case .timeline: setTimelineFeed(SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: savedSearchArticleIds!)), animated: true) } - + lastSearchString = searchString lastSearchScope = searchScope } - + } - + func findPrevArticle(_ article: Article) -> Article? { guard let index = articles.firstIndex(of: article), index > 0 else { return nil } return articles[index - 1] } - + func findNextArticle(_ article: Article) -> Article? { guard let index = articles.firstIndex(of: article), index + 1 != articles.count else { return nil } return articles[index + 1] } - + func selectPrevArticle() { if let article = prevArticle { selectArticle(article, animations: [.navigation, .scroll]) } } - + func selectNextArticle() { if let article = nextArticle { selectArticle(article, animations: [.navigation, .scroll]) } } - + func selectFirstUnread() { if selectFirstUnreadArticleInTimeline() { activityManager.selectingNextUnread() } } - + func selectPrevUnread() { - + // This should never happen, but I don't want to risk throwing us // into an infinite loop searching for an unread that isn't there. if appDelegate.unreadCount < 1 { return } - + isNavigationDisabled = true defer { isNavigationDisabled = false } - + if selectPrevUnreadArticleInTimeline() { return } - + selectPrevUnreadFeedFetcher() selectPrevUnreadArticleInTimeline() } func selectNextUnread() { - + // This should never happen, but I don't want to risk throwing us // into an infinite loop searching for an unread that isn't there. if appDelegate.unreadCount < 1 { return } - + isNavigationDisabled = true defer { isNavigationDisabled = false } - + if selectNextUnreadArticleInTimeline() { return } @@ -956,11 +956,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.mainTimelineViewController?.hideSearch() } - selectNextUnreadFeed() { + selectNextUnreadFeed { self.selectNextUnreadArticleInTimeline() } } - + func scrollOrGoToNextUnread() { if articleViewController?.canScrollDown() ?? false { articleViewController?.scrollPageDown() @@ -974,11 +974,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { articleViewController?.scrollPageUp() } } - + func markAllAsRead(_ articles: [Article], completion: (() -> Void)? = nil) { markArticlesWithUndo(articles, statusKey: .read, flag: true, completion: completion) } - + func markAllAsReadInTimeline(completion: (() -> Void)? = nil) { markAllAsRead(articles) { self.rootSplitViewController.show(.primary) @@ -1021,25 +1021,25 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { let articleBelowArray = articles.articlesBelow(article: article) markAllAsRead(articleBelowArray) } - + func markAsReadForCurrentArticle() { if let article = currentArticle { markArticlesWithUndo([article], statusKey: .read, flag: true) } } - + func markAsUnreadForCurrentArticle() { if let article = currentArticle { markArticlesWithUndo([article], statusKey: .read, flag: false) } } - + func toggleReadForCurrentArticle() { if let article = currentArticle { toggleRead(article) } } - + func toggleRead(_ article: Article) { guard !article.status.read || article.isAvailableToMarkUnread else { return } markArticlesWithUndo([article], statusKey: .read, flag: !article.status.read) @@ -1050,7 +1050,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { toggleStar(article) } } - + func toggleStar(_ article: Article) { markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred) } @@ -1079,7 +1079,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { if let parentFolder = parentFolder { markExpanded(parentFolder) } - + if let sidebarItemID = feed.sidebarItemID { self.treeControllerDelegate.addFilterException(sidebarItemID) } @@ -1087,7 +1087,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.treeControllerDelegate.addFilterException(parentFolderFeedID) } - rebuildBackingStores(initialLoad: initialLoad, completion: { + rebuildBackingStores(initialLoad: initialLoad, completion: { self.treeControllerDelegate.resetFilterExceptions() self.selectFeed(nil) { if self.rootSplitViewController.traitCollection.horizontalSizeClass == .compact { @@ -1107,14 +1107,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { self.rootSplitViewController.setNeedsStatusBarAppearanceUpdate() } } - + func hideStatusBar() { prefersStatusBarHidden = true UIView.animate(withDuration: 0.15) { self.rootSplitViewController.setNeedsStatusBarAppearanceUpdate() } } - + func showSettings(scrollToArticlesSection: Bool = false) { let settingsNavController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController let settingsViewController = settingsNavController.topViewController as! SettingsViewController @@ -1123,7 +1123,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { settingsViewController.presentingParentController = rootSplitViewController rootSplitViewController.present(settingsNavController, animated: true) } - + func showAccountInspector(for account: Account) { let accountInspectorNavController = UIStoryboard.inspector.instantiateViewController(identifier: "AccountInspectorNavigationViewController") as! UINavigationController @@ -1134,7 +1134,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { accountInspectorController.account = account rootSplitViewController.present(accountInspectorNavController, animated: true) } - + func showFeedInspector() { let timelineFeed = timelineFeed as? Feed let articleFeed = currentArticle?.feed @@ -1143,7 +1143,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } showFeedInspector(for: feed) } - + func showFeedInspector(for feed: Feed) { let feedInspectorNavController = UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController @@ -1153,30 +1153,30 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { feedInspectorController.feed = feed rootSplitViewController.present(feedInspectorNavController, animated: true) } - + func showAddFeed(initialFeed: String? = nil, initialFeedName: String? = nil) { - + // Since Add Feed can be opened from anywhere with a keyboard shortcut, we have to deselect any currently selected feeds selectFeed(nil) let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedViewControllerNav") as! UINavigationController - + let addViewController = addNavViewController.topViewController as! AddFeedViewController addViewController.initialFeed = initialFeed addViewController.initialFeedName = initialFeedName - + addNavViewController.modalPresentationStyle = .formSheet addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay mainFeedViewController.present(addNavViewController, animated: true) } - + func showAddFolder() { let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFolderViewControllerNav") as! UINavigationController addNavViewController.modalPresentationStyle = .formSheet addNavViewController.preferredContentSize = AddFolderViewController.preferredContentSizeForFormSheetDisplay mainFeedViewController.present(addNavViewController, animated: true) } - + func showFullScreenImage(image: UIImage, imageTitle: String?, transitioningDelegate: UIViewControllerTransitioningDelegate) { let imageVC = ImageViewController() imageVC.image = image @@ -1185,7 +1185,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { imageVC.transitioningDelegate = transitioningDelegate rootSplitViewController.present(imageVC, animated: true) } - + func homePageURLForFeed(_ indexPath: IndexPath) -> URL? { guard let node = nodeFor(indexPath), let feed = node.representedObject as? Feed, @@ -1195,19 +1195,19 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { } return url } - + func showBrowserForFeed(_ indexPath: IndexPath) { if let url = homePageURLForFeed(indexPath) { UIApplication.shared.open(url, options: [:]) } } - + func showBrowserForCurrentFeed() { if let ip = currentFeedIndexPath, let url = homePageURLForFeed(ip) { UIApplication.shared.open(url, options: [:]) } } - + func showBrowserForArticle(_ article: Article) { guard let url = article.preferredURL else { return } UIApplication.shared.open(url, options: [:]) @@ -1217,12 +1217,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { guard let url = currentArticle?.preferredURL else { return } UIApplication.shared.open(url, options: [:]) } - + func showInAppBrowser() { if currentArticle != nil { articleViewController?.openInAppBrowser() - } - else { + } else { mainFeedViewController.openInAppBrowser() } } @@ -1231,14 +1230,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { mainFeedViewController?.focus() selectArticle(nil) } - + func navigateToTimeline() { if currentArticle == nil && articles.count > 0 { selectArticle(articles[0]) } mainTimelineViewController?.focus() } - + func navigateToDetail() { articleViewController?.focus() } @@ -1246,22 +1245,22 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { func toggleSidebar() { rootSplitViewController.preferredDisplayMode = rootSplitViewController.displayMode == .oneBesideSecondary ? .secondaryOnly : .oneBesideSecondary } - + func selectArticleInCurrentFeed(_ articleID: String, isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { if let article = self.articles.first(where: { $0.articleID == articleID }) { self.selectArticle(article, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } } - + func importTheme(filename: String) { do { try ArticleThemeImporter.importTheme(controller: rootSplitViewController, url: URL(fileURLWithPath: filename)) } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) } - + } - + /// This will dismiss the foremost view controller if the user /// has launched from an external action (i.e., a widget tap, or /// selecting an article via a notification). @@ -1271,14 +1270,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner { /// otherwise, this function does nothing. func dismissIfLaunchingFromExternalAction() { guard let presentedController = mainFeedViewController.presentedViewController else { return } - + if presentedController.isKind(of: SFSafariViewController.self) { presentedController.dismiss(animated: true, completion: nil) } guard let settings = presentedController.children.first as? SettingsViewController else { return } settings.dismiss(animated: true, completion: nil) } - + } // MARK: UISplitViewControllerDelegate @@ -1307,7 +1306,7 @@ extension SceneCoordinator: UISplitViewControllerDelegate { return .primary } } - + func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) { var sidebarIsShowing = false @@ -1329,12 +1328,12 @@ extension SceneCoordinator: UISplitViewControllerDelegate { // MARK: UINavigationControllerDelegate extension SceneCoordinator: UINavigationControllerDelegate { - + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard UIApplication.shared.applicationState != .background else { return } - + guard rootSplitViewController.isCollapsed else { return } @@ -1377,7 +1376,7 @@ private extension SceneCoordinator { } runCommand(markReadCommand) } - + func updateUnreadCount() { var count = 0 for article in articles { @@ -1387,7 +1386,7 @@ private extension SceneCoordinator { } timelineUnreadCount = count } - + func rebuildArticleDictionaries() { var idDictionary = [String: Article]() @@ -1398,12 +1397,12 @@ private extension SceneCoordinator { _idToArticleDictionary = idDictionary articleDictionaryNeedsUpdate = false } - + func ensureFeedIsAvailableToSelect(_ feed: SidebarItem, completion: @escaping () -> Void) { addToFilterExceptionsIfNecessary(feed) addShadowTableToFilterExceptions() - - rebuildBackingStores(completion: { + + rebuildBackingStores(completion: { self.treeControllerDelegate.resetFilterExceptions() completion() }) @@ -1425,17 +1424,17 @@ private extension SceneCoordinator { } } } - + func addParentFolderToFilterExceptions(_ feed: SidebarItem) { guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject), let folder = node.parent?.representedObject as? Folder, let folderFeedID = folder.sidebarItemID else { return } - + treeControllerDelegate.addFilterException(folderFeedID) } - + func addShadowTableToFilterExceptions() { for section in shadowTable { for feedNode in section.feedNodes { @@ -1445,7 +1444,7 @@ private extension SceneCoordinator { } } } - + func queueRebuildBackingStores() { rebuildBackingStoresQueue.add(self, #selector(rebuildBackingStoresWithDefaults)) } @@ -1453,19 +1452,19 @@ private extension SceneCoordinator { @objc func rebuildBackingStoresWithDefaults() { rebuildBackingStores() } - + func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil, completion: (() -> Void)? = nil) { if !BatchUpdate.shared.isPerforming { addToFilterExceptionsIfNecessary(timelineFeed) treeController.rebuild() treeControllerDelegate.resetFilterExceptions() - + updateExpandedNodes?() let changes = rebuildShadowTable() mainFeedViewController.reloadFeeds(initialLoad: initialLoad, changes: changes, completion: completion) } } - + func rebuildShadowTable() -> ShadowTableChanges { var newShadowTable = [(sectionID: String, feedNodes: [FeedNode])]() @@ -1579,7 +1578,7 @@ private extension SceneCoordinator { } return false } - + func clearTimelineIfNoLongerAvailable() { if let feed = timelineFeed, !shadowTableContains(feed) { selectFeed(nil, deselectArticle: true) @@ -1592,18 +1591,18 @@ private extension SceneCoordinator { } return indexPathFor(node) } - + func setTimelineFeed(_ feed: SidebarItem?, animated: Bool, completion: (() -> Void)? = nil) { timelineFeed = feed - + fetchAndReplaceArticlesAsync(animated: animated) { self.mainTimelineViewController?.reinitializeArticles(resetScroll: true) completion?() } } - + func updateShowNamesAndIcons() { - + if timelineFeed is Feed { showFeedNames = { for article in articles { @@ -1621,12 +1620,12 @@ private extension SceneCoordinator { self.showIcons = true return } - + if showFeedNames == .none { self.showIcons = false return } - + for article in articles { if let authors = article.authors { for author in authors { @@ -1637,10 +1636,10 @@ private extension SceneCoordinator { } } } - + self.showIcons = false } - + func markExpanded(_ containerID: ContainerIdentifier) { expandedTable.insert(containerID) } @@ -1650,13 +1649,13 @@ private extension SceneCoordinator { markExpanded(containerID) } } - + func markExpanded(_ node: Node) { if let containerIdentifiable = node.representedObject as? ContainerIdentifiable { markExpanded(containerIdentifiable) } } - + func unmarkExpanded(_ containerID: ContainerIdentifier) { expandedTable.remove(containerID) } @@ -1684,16 +1683,16 @@ private extension SceneCoordinator { return articles.count - 1 } }() - + return selectPrevArticleInTimeline(startingRow: startingRow) } - + func selectPrevArticleInTimeline(startingRow: Int) -> Bool { - + guard startingRow >= 0 else { return false } - + for i in (0...startingRow).reversed() { let article = articles[i] if !article.status.read { @@ -1701,13 +1700,13 @@ private extension SceneCoordinator { return true } } - + return false - + } - + func selectPrevUnreadFeedFetcher() { - + let indexPath: IndexPath = { if currentFeedIndexPath == nil { return IndexPath(row: 0, section: 0) @@ -1728,20 +1727,20 @@ private extension SceneCoordinator { return IndexPath(row: indexPath.row - 1, section: indexPath.section) } }() - + if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) { return } let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1) selectPrevUnreadFeedFetcher(startingWith: maxIndexPath) - + } - + @discardableResult func selectPrevUnreadFeedFetcher(startingWith indexPath: IndexPath) -> Bool { - + for i in (0...indexPath.section).reversed() { - + let startingRow: Int = { if indexPath.section == i { return indexPath.row @@ -1749,39 +1748,39 @@ private extension SceneCoordinator { return shadowTable[i].feedNodes.count - 1 } }() - + for j in (0...startingRow).reversed() { - + let prevIndexPath = IndexPath(row: j, section: i) guard let node = nodeFor(prevIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else { assertionFailure() return true } - + if isExpanded(node) { continue } - + if unreadCountProvider.unreadCount > 0 { selectFeed(indexPath: prevIndexPath, animations: [.scroll, .navigation]) return true } - + } - + } - + return false - + } - + // MARK: Select Next Unread - + @discardableResult func selectFirstUnreadArticleInTimeline() -> Bool { return selectNextArticleInTimeline(startingRow: 0, animated: true) } - + @discardableResult func selectNextUnreadArticleInTimeline() -> Bool { let startingRow: Int = { @@ -1791,16 +1790,16 @@ private extension SceneCoordinator { return 0 } }() - + return selectNextArticleInTimeline(startingRow: startingRow, animated: false) } - + func selectNextArticleInTimeline(startingRow: Int, animated: Bool) -> Bool { - + guard startingRow < articles.count else { return false } - + for i in startingRow.. Void) { - + let indexPath: IndexPath = { if currentFeedIndexPath == nil { return IndexPath(row: -1, section: 0) @@ -1822,7 +1821,7 @@ private extension SceneCoordinator { return currentFeedIndexPath! } }() - + // Increment or wrap around the IndexPath let nextIndexPath: IndexPath = { if indexPath.row + 1 >= shadowTable[indexPath.section].feedNodes.count { @@ -1835,7 +1834,7 @@ private extension SceneCoordinator { return IndexPath(row: indexPath.row + 1, section: indexPath.section) } }() - + selectNextUnreadFeed(startingWith: nextIndexPath) { found in if !found { self.selectNextUnreadFeed(startingWith: IndexPath(row: 0, section: 0)) { _ in @@ -1845,13 +1844,13 @@ private extension SceneCoordinator { completion() } } - + } - + func selectNextUnreadFeed(startingWith indexPath: IndexPath, completion: @escaping (Bool) -> Void) { - + for i in indexPath.section.. 0 { selectFeed(indexPath: nextIndexPath, animations: [.scroll, .navigation], deselectArticle: false) { self.currentArticle = nil @@ -1880,32 +1879,32 @@ private extension SceneCoordinator { } return } - + } - + } - + completion(false) - + } - + // MARK: Fetching Articles - + func emptyTheTimeline() { if !articles.isEmpty { replaceArticles(with: Set
(), animated: false) } } - + func sortParametersDidChange() { replaceArticles(with: Set(articles), animated: true) } - + func replaceArticles(with unsortedArticles: Set
, animated: Bool) { let sortedArticles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) replaceArticles(with: sortedArticles, animated: animated) } - + func replaceArticles(with sortedArticles: ArticleArray, animated: Bool) { if articles != sortedArticles { articles = sortedArticles @@ -1914,7 +1913,7 @@ private extension SceneCoordinator { mainTimelineViewController?.reloadArticles(animated: animated) } } - + func queueFetchAndMergeArticles() { fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticlesAsync)) } @@ -1925,13 +1924,13 @@ private extension SceneCoordinator { self.mainTimelineViewController?.restoreSelectionIfNecessary(adjustScroll: false) } } - + func fetchAndMergeArticlesAsync(animated: Bool = true, completion: (() -> Void)? = nil) { - + guard let timelineFeed = timelineFeed else { return } - + fetchUnsortedArticlesAsync(for: [timelineFeed]) { [weak self] (unsortedArticles) in // Merge articles by articleID. For any unique articleID in current articles, add to unsortedArticles. guard let strongSelf = self else { @@ -1951,9 +1950,9 @@ private extension SceneCoordinator { strongSelf.replaceArticles(with: updatedArticles, animated: animated) completion?() } - + } - + func cancelPendingAsyncFetches() { fetchSerialNumber += 1 fetchRequestQueue.cancelAllRequests() @@ -1963,21 +1962,21 @@ private extension SceneCoordinator { // To be called when we need to do an entire fetch, but an async delay is okay. // Example: we have the Today feed selected, and the calendar day just changed. cancelPendingAsyncFetches() - + emptyTheTimeline() - + guard let timelineFeed = timelineFeed else { completion() return } - + var fetchers = [ArticleFetcher]() fetchers.append(timelineFeed) if exceptionArticleFetcher != nil { fetchers.append(exceptionArticleFetcher!) exceptionArticleFetcher = nil } - + fetchUnsortedArticlesAsync(for: fetchers) { [weak self] (articles) in self?.replaceArticles(with: articles, animated: animated) completion() @@ -1989,7 +1988,7 @@ private extension SceneCoordinator { // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. precondition(Thread.isMainThread) cancelPendingAsyncFetches() - + let fetchers = representedObjects.compactMap { $0 as? ArticleFetcher } let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilterEnabledTable: readFilterEnabledTable, fetchers: fetchers) { [weak self] (articles, operation) in precondition(Thread.isMainThread) @@ -1998,7 +1997,7 @@ private extension SceneCoordinator { } completion(articles) } - + fetchRequestQueue.add(fetchOperation) } @@ -2008,18 +2007,18 @@ private extension SceneCoordinator { } return false } - + func timelineFetcherContainsAnyFolder() -> Bool { if timelineFeed is Folder { return true } return false } - + func timelineFetcherContainsAnyFeed(_ feeds: Set) -> Bool { - + // Return true if there’s a match or if a folder contains (recursively) one of feeds - + if let feed = timelineFeed as? Feed { for oneFeed in feeds { if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url { @@ -2033,13 +2032,13 @@ private extension SceneCoordinator { } } } - + return false - + } - + // MARK: NSUserActivity - + func windowState() -> [AnyHashable: Any] { let containerExpandedWindowState = expandedTable.map( { $0.userInfo }) var readArticlesFilterState = [[AnyHashable: AnyHashable]: Bool]() @@ -2052,23 +2051,23 @@ private extension SceneCoordinator { UserInfoKey.readArticlesFilterState: readArticlesFilterState ] } - - func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { + + func handleSelectFeed(_ userInfo: [AnyHashable: Any]?) { guard let userInfo = userInfo, - let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], + let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable: AnyHashable], let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo) else { return } treeControllerDelegate.addFilterException(feedIdentifier) - + switch feedIdentifier { - + case .smartFeed: guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return } markExpanded(SmartFeedsController.shared) - rebuildBackingStores(initialLoad: true, completion: { + rebuildBackingStores(initialLoad: true, completion: { self.treeControllerDelegate.resetFilterExceptions() if let indexPath = self.indexPathFor(smartFeed) { self.selectFeed(indexPath: indexPath) { @@ -2076,10 +2075,10 @@ private extension SceneCoordinator { } } }) - + case .script: break - + case .folder(let accountID, let folderName): guard let accountNode = self.findAccountNode(accountID: accountID), let account = accountNode.representedObject as? Account else { @@ -2087,34 +2086,34 @@ private extension SceneCoordinator { } markExpanded(account) - - rebuildBackingStores(initialLoad: true, completion: { + + rebuildBackingStores(initialLoad: true, completion: { self.treeControllerDelegate.resetFilterExceptions() - + if let folderNode = self.findFolderNode(folderName: folderName, beginningAt: accountNode), let indexPath = self.indexPathFor(folderNode) { self.selectFeed(indexPath: indexPath) { self.mainFeedViewController.focus() } } }) - + case .feed(let accountID, let feedID): guard let accountNode = findAccountNode(accountID: accountID), let account = accountNode.representedObject as? Account, let feed = account.existingFeed(withFeedID: feedID) else { return } - + self.discloseFeed(feed, initialLoad: true) { self.mainFeedViewController.focus() } } } - - func handleReadArticle(_ userInfo: [AnyHashable : Any]?) { + + func handleReadArticle(_ userInfo: [AnyHashable: Any]?) { guard let userInfo = userInfo else { return } - - guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], + + guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any], let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountName = articlePathUserInfo[ArticlePathKey.accountName] as? String, let feedID = articlePathUserInfo[ArticlePathKey.feedID] as? String, @@ -2123,24 +2122,24 @@ private extension SceneCoordinator { let account = accountNode.representedObject as? Account else { return } - + exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) if restoreFeedSelection(userInfo, accountID: accountID, feedID: feedID, articleID: articleID) { return } - + guard let feed = account.existingFeed(withFeedID: feedID) else { return } - + discloseFeed(feed) { self.selectArticleInCurrentFeed(articleID) } } - - func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, feedID: String, articleID: String) -> Bool { - guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], + + func restoreFeedSelection(_ userInfo: [AnyHashable: Any], accountID: String, feedID: String, articleID: String) -> Bool { + guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable: AnyHashable], let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo), let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool, let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else { @@ -2158,7 +2157,7 @@ private extension SceneCoordinator { treeControllerDelegate.addFilterException(feedIdentifier) } return found - + case .feed: let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) if found { @@ -2168,11 +2167,11 @@ private extension SceneCoordinator { } } return found - + } - + } - + func findAccountNode(accountID: String, accountName: String? = nil) -> Node? { if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) { return node @@ -2184,7 +2183,7 @@ private extension SceneCoordinator { return nil } - + func findFolderNode(folderName: String, beginningAt startingNode: Node) -> Node? { if let node = startingNode.descendantNode(where: { ($0.representedObject as? Folder)?.nameForDisplay == folderName }) { return node @@ -2198,15 +2197,15 @@ private extension SceneCoordinator { } return nil } - + func selectFeedAndArticle(feedIdentifier: SidebarItemIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool { guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false } - + selectFeed(indexPath: feedIndexPath) { self.selectArticleInCurrentFeed(articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } - + return true } - + } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index b5086e77d..b5db0d04d 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -11,18 +11,18 @@ import UserNotifications import Account class SceneDelegate: UIResponder, UIWindowSceneDelegate { - + var window: UIWindow? var coordinator: SceneCoordinator! - + // UIWindowScene delegate - + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - + window!.tintColor = AppAssets.primaryAccentColor updateUserInterfaceStyle() UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance() - + let rootViewController = window!.rootViewController as! RootSplitViewController rootViewController.presentsWithGesture = true rootViewController.showsSecondaryOnlyButton = true @@ -38,43 +38,43 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { coordinator = SceneCoordinator(rootSplitViewController: rootViewController) rootViewController.coordinator = coordinator rootViewController.delegate = coordinator - + coordinator.restoreWindowState(session.stateRestorationActivity) - + NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) - - if let _ = connectionOptions.urlContexts.first?.url { + + if let _ = connectionOptions.urlContexts.first?.url { self.scene(scene, openURLContexts: connectionOptions.urlContexts) return } - + if let shortcutItem = connectionOptions.shortcutItem { handleShortcutItem(shortcutItem) return } - + if let notificationResponse = connectionOptions.notificationResponse { coordinator.handle(notificationResponse) return } - + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { coordinator.handle(userActivity) } } - + func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { appDelegate.resumeDatabaseProcessingIfNecessary() handleShortcutItem(shortcutItem) completionHandler(true) } - + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { appDelegate.resumeDatabaseProcessingIfNecessary() coordinator.handle(userActivity) } - + func sceneDidEnterBackground(_ scene: UIScene) { ArticleStringFormatter.emptyCaches() appDelegate.prepareAccountsForBackground() @@ -85,39 +85,39 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { appDelegate.prepareAccountsForForeground() coordinator.resetFocus() } - + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { return coordinator.stateRestorationActivity } - + // API - + func handle(_ response: UNNotificationResponse) { appDelegate.resumeDatabaseProcessingIfNecessary() coordinator.handle(response) } - + func suspend() { coordinator.suspend() } - + func cleanUp(conditional: Bool) { coordinator.cleanUp(conditional: conditional) } - + // Handle Opening of URLs - + func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { guard let context = urlContexts.first else { return } - + DispatchQueue.main.async { - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.coordinator.dismissIfLaunchingFromExternalAction() } - + let urlString = context.url.absoluteString - + // Handle the feed: and feeds: schemes if urlString.starts(with: "feed:") || urlString.starts(with: "feeds:") { let normalizedURLString = urlString.normalizedURL @@ -125,7 +125,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.coordinator.showAddFeed(initialFeed: normalizedURLString, initialFeedName: nil) } } - + // Show Unread View or Article if urlString.contains(WidgetDeepLink.unread.url.absoluteString) { guard let comps = URLComponents(string: urlString ) else { return } @@ -134,14 +134,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if AccountManager.shared.isSuspended { AccountManager.shared.resumeAll() } - self.coordinator.selectAllUnreadFeed() { + self.coordinator.selectAllUnreadFeed { self.coordinator.selectArticleInCurrentFeed(id!) } } else { self.coordinator.selectAllUnreadFeed() } } - + // Show Today View or Article if urlString.contains(WidgetDeepLink.today.url.absoluteString) { guard let comps = URLComponents(string: urlString ) else { return } @@ -150,14 +150,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if AccountManager.shared.isSuspended { AccountManager.shared.resumeAll() } - self.coordinator.selectTodayFeed() { + self.coordinator.selectTodayFeed { self.coordinator.selectArticleInCurrentFeed(id!) } } else { self.coordinator.selectTodayFeed() } } - + // Show Starred View or Article if urlString.contains(WidgetDeepLink.starred.url.absoluteString) { guard let comps = URLComponents(string: urlString ) else { return } @@ -166,38 +166,38 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if AccountManager.shared.isSuspended { AccountManager.shared.resumeAll() } - self.coordinator.selectStarredFeed() { + self.coordinator.selectStarredFeed { self.coordinator.selectArticleInCurrentFeed(id!) } } else { self.coordinator.selectStarredFeed() } } - + let filename = context.url.standardizedFileURL.path if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) { self.coordinator.importTheme(filename: filename) return } - + // Handle theme URLs: netnewswire://theme/add?url={url} guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false), "theme" == comps.host, let queryItems = comps.queryItems else { return } - + if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value { if let themeURL = URL(string: providedThemeURL) { let request = URLRequest(url: themeURL) - + DispatchQueue.main.async { NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) } - let task = URLSession.shared.downloadTask(with: request) { location, response, error in + let task = URLSession.shared.downloadTask(with: request) { location, _, error in guard let location = location else { return } - + do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { @@ -212,14 +212,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } else { return } - - + } } } private extension SceneDelegate { - + func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { switch shortcutItem.type { case "com.ranchero.NetNewsWire.FirstUnread": @@ -232,11 +231,11 @@ private extension SceneDelegate { break } } - + @objc func userDefaultsDidChange() { updateUserInterfaceStyle() } - + func updateUserInterfaceStyle() { DispatchQueue.main.async { switch AppDefaults.userInterfaceColorPalette { @@ -249,7 +248,5 @@ private extension SceneDelegate { } } } - - - + } diff --git a/iOS/Settings/AboutViewController.swift b/iOS/Settings/AboutViewController.swift index a6fb5d8ba..c43afbfcd 100644 --- a/iOS/Settings/AboutViewController.swift +++ b/iOS/Settings/AboutViewController.swift @@ -15,11 +15,11 @@ class AboutViewController: UITableViewController { @IBOutlet weak var acknowledgmentsTextView: UITextView! @IBOutlet weak var thanksTextView: UITextView! @IBOutlet weak var dedicationTextView: UITextView! - + override func viewDidLoad() { - + super.viewDidLoad() - + configureCell(file: "About", textView: aboutTextView) configureCell(file: "Credits", textView: creditsTextView) configureCell(file: "Thanks", textView: thanksTextView) @@ -32,7 +32,7 @@ class AboutViewController: UITableViewController { buildLabel.numberOfLines = 0 buildLabel.sizeToFit() buildLabel.translatesAutoresizingMaskIntoConstraints = false - + let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) wrapperView.translatesAutoresizingMaskIntoConstraints = false wrapperView.addSubview(buildLabel) @@ -42,11 +42,11 @@ class AboutViewController: UITableViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } - + } private extension AboutViewController { - + func configureCell(file: String, textView: UITextView) { let url = Bundle.main.url(forResource: file, withExtension: "rtf")! let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) @@ -55,5 +55,5 @@ private extension AboutViewController { textView.adjustsFontForContentSizeCategory = true textView.font = .preferredFont(forTextStyle: .body) } - + } diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift index 8f0e92e44..89e5e7ecd 100644 --- a/iOS/Settings/AddAccountViewController.swift +++ b/iOS/Settings/AddAccountViewController.swift @@ -21,7 +21,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate case icloud case web case selfhosted - + var sectionHeader: String { switch self { case .local: @@ -34,7 +34,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate return NSLocalizedString("Self-hosted", comment: "Self hosted Account") } } - + var sectionFooter: String { switch self { case .local: @@ -47,7 +47,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices", comment: "Self hosted Account") } } - + var sectionContent: [AccountType] { switch self { case .local: @@ -65,35 +65,31 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate } } } - - override func viewDidLoad() { - super.viewDidLoad() - } override func numberOfSections(in tableView: UITableView) -> Int { return AddAccountSections.allCases.count } - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == AddAccountSections.local.rawValue { return AddAccountSections.local.sectionContent.count } - + if section == AddAccountSections.icloud.rawValue { return AddAccountSections.icloud.sectionContent.count } - + if section == AddAccountSections.web.rawValue { return AddAccountSections.web.sectionContent.count } - + if section == AddAccountSections.selfhosted.rawValue { return AddAccountSections.selfhosted.sectionContent.count } return 0 } - + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch section { case AddAccountSections.local.rawValue: @@ -108,7 +104,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate return nil } } - + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { switch section { case AddAccountSections.local.rawValue: @@ -123,10 +119,10 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate return nil } } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsComboTableViewCell - + switch indexPath.section { case AddAccountSections.local.rawValue: cell.comboNameLabel?.text = AddAccountSections.local.sectionContent[indexPath.row].localizedAccountName() @@ -149,15 +145,15 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate case AddAccountSections.selfhosted.rawValue: cell.comboNameLabel?.text = AddAccountSections.selfhosted.sectionContent[indexPath.row].localizedAccountName() cell.comboImage?.image = AppAssets.image(for: AddAccountSections.selfhosted.sectionContent[indexPath.row]) - + default: return cell } return cell } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - + switch indexPath.section { case AddAccountSections.local.rawValue: let type = AddAccountSections.local.sectionContent[indexPath.row] @@ -175,7 +171,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate return } } - + private func presentController(for accountType: AccountType) { switch accountType { case .onMyMac: @@ -216,18 +212,18 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate present(navController, animated: true) } } - + func dismiss() { navigationController?.popViewController(animated: false) } - + } extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate { - + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { let rootViewController = view.window?.rootViewController - + account.refreshAll { result in switch result { case .success: @@ -239,10 +235,10 @@ extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate { viewController.presentError(error) } } - + dismiss() } - + func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { presentError(error) } diff --git a/iOS/Settings/ArticleThemeImporter.swift b/iOS/Settings/ArticleThemeImporter.swift index 079d67f09..8a8a787c5 100644 --- a/iOS/Settings/ArticleThemeImporter.swift +++ b/iOS/Settings/ArticleThemeImporter.swift @@ -9,7 +9,7 @@ import UIKit struct ArticleThemeImporter { - + static func importTheme(controller: UIViewController, url: URL) throws { let theme = try ArticleTheme(url: url, isAppTheme: false) @@ -20,13 +20,13 @@ struct ArticleThemeImporter { let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.creatorHomePage) as String let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) - + if let websiteURL = URL(string: theme.creatorHomePage) { let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") - let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in + let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { _ in UIApplication.shared.open(websiteURL) try? Self.importTheme(controller: controller, url: url) } @@ -49,7 +49,7 @@ struct ArticleThemeImporter { } let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme") - let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { action in + let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { _ in if ArticleThemesManager.shared.themeExists(filename: url.path) { let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme") @@ -61,7 +61,7 @@ struct ArticleThemeImporter { let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) - let overwriteAction = UIAlertAction(title: NSLocalizedString("Overwrite", comment: "Overwrite"), style: .default) { action in + let overwriteAction = UIAlertAction(title: NSLocalizedString("Overwrite", comment: "Overwrite"), style: .default) { _ in importTheme() } alertController.addAction(overwriteAction) @@ -71,32 +71,32 @@ struct ArticleThemeImporter { } else { importTheme() } - + } - + alertController.addAction(installThemeAction) alertController.preferredAction = installThemeAction controller.present(alertController, animated: true) } - + } private extension ArticleThemeImporter { - + static func confirmImportSuccess(controller: UIViewController, themeName: String) { let title = NSLocalizedString("Theme installed", comment: "Theme installed") - + let localizedMessageText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed") let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - + let doneTitle = NSLocalizedString("Done", comment: "Done") alertController.addAction(UIAlertAction(title: doneTitle, style: .default)) - + controller.present(alertController, animated: true) } - + } diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index 7a153b26a..74fe144b6 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -17,15 +17,15 @@ extension UTType { class ArticleThemesTableViewController: UITableViewController { override func viewDidLoad() { - let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:))); - importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme"); + let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:))) + importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme") navigationItem.rightBarButtonItem = importBarButtonItem - + NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) } - + // MARK: Notifications - + @objc func articleThemeNamesDidChangeNotification(_ note: Notification) { tableView.reloadData() } @@ -49,21 +49,21 @@ class ArticleThemesTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - + let themeName: String if indexPath.row == 0 { themeName = ArticleTheme.defaultTheme.name } else { themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1] } - + cell.textLabel?.text = themeName if themeName == ArticleThemesManager.shared.currentTheme.name { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } - + return cell } @@ -80,33 +80,33 @@ class ArticleThemesTableViewController: UITableViewController { !theme.isAppTheme else { return nil } let deleteTitle = NSLocalizedString("Delete", comment: "Delete") - let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in + let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (_, _, completion) in let title = NSLocalizedString("Delete Theme?", comment: "Delete Theme") - + let localizedMessageText = NSLocalizedString("Are you sure you want to delete the theme “%@”?.", comment: "Delete Theme Message") let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") - let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { action in + let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in completion(true) } alertController.addAction(cancelAction) let deleteTitle = NSLocalizedString("Delete", comment: "Delete") - let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { action in + let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { _ in ArticleThemesManager.shared.deleteTheme(themeName: themeName) completion(true) } alertController.addAction(deleteAction) - + self?.present(alertController, animated: true) } - + deleteAction.image = AppAssets.trashImage deleteAction.backgroundColor = UIColor.systemRed - + return UISwipeActionsConfiguration(actions: [deleteAction]) } } @@ -114,12 +114,12 @@ class ArticleThemesTableViewController: UITableViewController { // MARK: UIDocumentPickerDelegate extension ArticleThemesTableViewController: UIDocumentPickerDelegate { - + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } if url.startAccessingSecurityScopedResource() { - + defer { url.stopAccessingSecurityScopedResource() } diff --git a/iOS/Settings/SettingsComboTableViewCell.swift b/iOS/Settings/SettingsComboTableViewCell.swift index 9e0200a27..230aa5f3e 100644 --- a/iOS/Settings/SettingsComboTableViewCell.swift +++ b/iOS/Settings/SettingsComboTableViewCell.swift @@ -16,7 +16,7 @@ class SettingsComboTableViewCell: VibrantTableViewCell { override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) updateLabelVibrancy(comboNameLabel, color: labelColor, animated: animated) - + let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label if animated { UIView.animate(withDuration: Self.duration) { @@ -26,5 +26,5 @@ class SettingsComboTableViewCell: VibrantTableViewCell { self.comboImage?.tintColor = tintColor } } - + } diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index bfb9fed89..f0bfc502f 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -16,7 +16,7 @@ import UniformTypeIdentifiers class SettingsViewController: UITableViewController { private weak var opmlAccount: Account? - + @IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch! @IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch! @@ -25,10 +25,10 @@ class SettingsViewController: UITableViewController { @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch! @IBOutlet weak var colorPaletteDetailLabel: UILabel! @IBOutlet weak var openLinksInNetNewsWire: UISwitch! - + var scrollToArticlesSection = false weak var presentingParentController: UIViewController? - + override func viewDidLoad() { // This hack mostly works around a bug in static tables with dynamic type. See: https://spin.atomicobject.com/2018/10/15/dynamic-type-static-uitableview/ NotificationCenter.default.removeObserver(tableView!, name: UIContentSizeCategory.didChangeNotification, object: nil) @@ -40,14 +40,14 @@ class SettingsViewController: UITableViewController { tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell") tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell") - + tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 44 } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + if AppDefaults.shared.timelineSortDirection == .orderedAscending { timelineSortOrderSwitch.isOn = true } else { @@ -66,7 +66,6 @@ class SettingsViewController: UITableViewController { refreshClearsReadArticlesSwitch.isOn = false } - articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name if AppDefaults.shared.confirmMarkAllAsRead { @@ -80,11 +79,10 @@ class SettingsViewController: UITableViewController { } else { showFullscreenArticlesSwitch.isOn = false } - + colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette) - + openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser - let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) buildLabel.font = UIFont.systemFont(ofSize: 11.0) @@ -92,27 +90,27 @@ class SettingsViewController: UITableViewController { buildLabel.text = "\(Bundle.main.appName) \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))" buildLabel.sizeToFit() buildLabel.translatesAutoresizingMaskIntoConstraints = false - + let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) wrapperView.translatesAutoresizingMaskIntoConstraints = false wrapperView.addSubview(buildLabel) tableView.tableFooterView = wrapperView } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) - + if scrollToArticlesSection { tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true) scrollToArticlesSection = false } } - + // MARK: UITableView - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch section { @@ -237,7 +235,7 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return false } - + override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { return false } @@ -245,21 +243,21 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .none } - + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } - + override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1)) } - + // MARK: Actions - + @IBAction func done(_ sender: Any) { dismiss(animated: true) } - + @IBAction func switchTimelineOrder(_ sender: Any) { if timelineSortOrderSwitch.isOn { AppDefaults.shared.timelineSortDirection = .orderedAscending @@ -267,7 +265,7 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.timelineSortDirection = .orderedDescending } } - + @IBAction func switchGroupByFeed(_ sender: Any) { if groupByFeedSwitch.isOn { AppDefaults.shared.timelineGroupByFeed = true @@ -275,7 +273,7 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.timelineGroupByFeed = false } } - + @IBAction func switchClearsReadArticles(_ sender: Any) { if refreshClearsReadArticlesSwitch.isOn { AppDefaults.shared.refreshClearsReadArticles = true @@ -283,7 +281,7 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.refreshClearsReadArticles = false } } - + @IBAction func switchConfirmMarkAllAsRead(_ sender: Any) { if confirmMarkAllAsReadSwitch.isOn { AppDefaults.shared.confirmMarkAllAsRead = true @@ -291,7 +289,7 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.confirmMarkAllAsRead = false } } - + @IBAction func switchFullscreenArticles(_ sender: Any) { if showFullscreenArticlesSwitch.isOn { AppDefaults.shared.articleFullscreenAvailable = true @@ -299,7 +297,7 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.articleFullscreenAvailable = false } } - + @IBAction func switchBrowserPreference(_ sender: Any) { if openLinksInNetNewsWire.isOn { AppDefaults.shared.useSystemBrowser = false @@ -307,14 +305,13 @@ class SettingsViewController: UITableViewController { AppDefaults.shared.useSystemBrowser = true } } - - + // MARK: Notifications - + @objc func contentSizeCategoryDidChange() { tableView.reloadData() } - + @objc func accountsDidChange() { tableView.reloadData() } @@ -322,17 +319,17 @@ class SettingsViewController: UITableViewController { @objc func displayNameDidChange() { tableView.reloadData() } - + @objc func browserPreferenceDidChange() { tableView.reloadData() } - + } // MARK: OPML Document Picker extension SettingsViewController: UIDocumentPickerDelegate { - + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { for url in urls { opmlAccount?.importOPML(url) { result in @@ -347,13 +344,13 @@ extension SettingsViewController: UIDocumentPickerDelegate { } } } - + } // MARK: Private private extension SettingsViewController { - + func addFeed() { self.dismiss(animated: true) @@ -363,10 +360,10 @@ private extension SettingsViewController { addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News") addNavViewController.modalPresentationStyle = .formSheet addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay - + presentingParentController?.present(addNavViewController, animated: true) } - + func importOPML(sourceView: UIView, sourceRect: CGRect) { switch AccountManager.shared.activeAccounts.count { case 0: @@ -378,18 +375,18 @@ private extension SettingsViewController { importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) } } - + func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { let title = NSLocalizedString("Choose an account to receive the imported feeds and folders", comment: "Import Account") let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) - + if let popoverController = alert.popoverPresentationController { popoverController.sourceView = view popoverController.sourceRect = sourceRect } for account in AccountManager.shared.sortedActiveAccounts { - let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in + let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] _ in self?.opmlAccount = account self?.importOPMLDocumentPicker() } @@ -401,15 +398,15 @@ private extension SettingsViewController { self.present(alert, animated: true) } - + func importOPMLDocumentPicker() { - + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.opml, UTType.xml], asCopy: true) documentPicker.delegate = self documentPicker.modalPresentationStyle = .formSheet self.present(documentPicker, animated: true) } - + func exportOPML(sourceView: UIView, sourceRect: CGRect) { if AccountManager.shared.accounts.count == 1 { opmlAccount = AccountManager.shared.accounts.first! @@ -418,18 +415,18 @@ private extension SettingsViewController { exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) } } - + func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { let title = NSLocalizedString("Choose an account with the subscriptions to export", comment: "Export Account") let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) - + if let popoverController = alert.popoverPresentationController { popoverController.sourceView = view popoverController.sourceRect = sourceRect } for account in AccountManager.shared.sortedAccounts { - let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in + let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] _ in self?.opmlAccount = account self?.exportOPMLDocumentPicker() } @@ -441,10 +438,10 @@ private extension SettingsViewController { self.present(alert, animated: true) } - + func exportOPMLDocumentPicker() { guard let account = opmlAccount else { return } - + let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) let filename = "Subscriptions-\(accountName).opml" let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename) @@ -454,16 +451,16 @@ private extension SettingsViewController { } catch { self.presentError(title: "OPML Export Error", message: error.localizedDescription) } - + let documentPicker = UIDocumentPickerViewController(forExporting: [tempFile]) documentPicker.modalPresentationStyle = .formSheet self.present(documentPicker, animated: true) } - + func openURL(_ urlString: String) { let vc = SFSafariViewController(url: URL(string: urlString)!) vc.modalPresentationStyle = .pageSheet present(vc, animated: true) } - + } diff --git a/iOS/Settings/TimelineCustomizerViewController.swift b/iOS/Settings/TimelineCustomizerViewController.swift index 7a76feaa5..ea2ff895e 100644 --- a/iOS/Settings/TimelineCustomizerViewController.swift +++ b/iOS/Settings/TimelineCustomizerViewController.swift @@ -14,15 +14,15 @@ class TimelineCustomizerViewController: UIViewController { @IBOutlet weak var iconSizeSlider: TickMarkSlider! @IBOutlet weak var numberOfLinesSliderContainerView: UIView! @IBOutlet weak var numberOfLinesSlider: TickMarkSlider! - + @IBOutlet weak var previewWidthConstraint: NSLayoutConstraint! @IBOutlet weak var previewHeightConstraint: NSLayoutConstraint! - + @IBOutlet weak var previewContainerView: UIView! var previewController: TimelinePreviewTableViewController { return children.first as! TimelinePreviewTableViewController } - + override func viewDidLoad() { super.viewDidLoad() @@ -34,13 +34,13 @@ class TimelineCustomizerViewController: UIViewController { numberOfLinesSlider.value = Float(AppDefaults.shared.timelineNumberOfLines) numberOfLinesSlider.addTickMarks() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updatePreviewBorder() updatePreview() } - + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { updatePreviewBorder() updatePreview() @@ -51,18 +51,18 @@ class TimelineCustomizerViewController: UIViewController { AppDefaults.shared.timelineIconSize = iconSize updatePreview() } - + @IBAction func numberOfLinesChanged(_ sender: Any) { AppDefaults.shared.timelineNumberOfLines = Int(numberOfLinesSlider.value.rounded()) updatePreview() } - + } // MARK: Private private extension TimelineCustomizerViewController { - + func updatePreview() { let previewWidth: CGFloat = { if traitCollection.userInterfaceIdiom == .phone { @@ -71,13 +71,13 @@ private extension TimelineCustomizerViewController { return view.bounds.width / 1.5 } }() - + previewWidthConstraint.constant = previewWidth previewHeightConstraint.constant = previewController.heightFor(width: previewWidth) - + previewController.reload() } - + func updatePreviewBorder() { if traitCollection.userInterfaceStyle == .dark { previewContainerView.layer.borderColor = UIColor.black.cgColor @@ -86,5 +86,5 @@ private extension TimelineCustomizerViewController { previewContainerView.layer.borderWidth = 0 } } - + } diff --git a/iOS/Settings/TimelinePreviewTableViewController.swift b/iOS/Settings/TimelinePreviewTableViewController.swift index 4c473fe05..f0c2713c2 100644 --- a/iOS/Settings/TimelinePreviewTableViewController.swift +++ b/iOS/Settings/TimelinePreviewTableViewController.swift @@ -12,13 +12,13 @@ import Articles class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { @IBOutlet weak var tableView: UITableView! - + override func viewDidLoad() { super.viewDidLoad() - + tableView.delegate = self tableView.dataSource = self - + } func heightFor(width: CGFloat) -> CGFloat { @@ -46,7 +46,7 @@ class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, cell.cellData = prototypeCellData return cell } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.selectRow(at: nil, animated: true, scrollPosition: .none) } @@ -64,14 +64,14 @@ private extension TimelinePreviewTableViewController { var prototypeCellData: MainTimelineCellData { let longTitle = "Enim ut tellus elementum sagittis vitae et. Nibh praesent tristique magna sit amet purus gravida quis blandit. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Massa id neque aliquam vestibulum morbi blandit. Ultrices vitae auctor eu augue. Enim eu turpis egestas pretium aenean pharetra magna. Eget gravida cum sociis natoque. Sit amet consectetur adipiscing elit. Auctor eu augue ut lectus arcu bibendum. Maecenas volutpat blandit aliquam etiam erat velit. Ut pharetra sit amet aliquam id diam maecenas ultricies. In hac habitasse platea dictumst quisque sagittis purus sit amet." - + let prototypeID = "prototype" let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date()) let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) - + let iconImage = IconImage(AppAssets.faviconTemplateImage.withTintColor(AppAssets.secondaryAccentColor)) - + return MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Feed Name", byline: nil, iconImage: iconImage, showIcon: true, numberOfLines: AppDefaults.shared.timelineNumberOfLines, iconSize: AppDefaults.shared.timelineIconSize) } - + } diff --git a/iOS/ShareExtension/ShareFolderPickerCell.swift b/iOS/ShareExtension/ShareFolderPickerCell.swift index 6d37f8365..a8566646c 100644 --- a/iOS/ShareExtension/ShareFolderPickerCell.swift +++ b/iOS/ShareExtension/ShareFolderPickerCell.swift @@ -9,7 +9,7 @@ import UIKit class ShareFolderPickerCell: UITableViewCell { - + @IBOutlet weak var icon: UIImageView! @IBOutlet weak var label: UILabel! } diff --git a/iOS/ShareExtension/ShareFolderPickerController.swift b/iOS/ShareExtension/ShareFolderPickerController.swift index 7c1319060..85f18ef81 100644 --- a/iOS/ShareExtension/ShareFolderPickerController.swift +++ b/iOS/ShareExtension/ShareFolderPickerController.swift @@ -20,13 +20,13 @@ class ShareFolderPickerController: UITableViewController { var selectedContainerID: ContainerIdentifier? weak var delegate: ShareFolderPickerControllerDelegate? - + override func viewDidLoad() { tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell") tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell") - + } - + override func numberOfSections(in tableView: UITableView) -> Int { return 1 } @@ -34,7 +34,7 @@ class ShareFolderPickerController: UITableViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return containers?.count ?? 0 } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let container = containers?[indexPath.row] let cell: ShareFolderPickerCell = { @@ -44,7 +44,7 @@ class ShareFolderPickerController: UITableViewController { return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell } }() - + if let account = container as? ExtensionAccount { cell.icon.image = AppAssets.image(for: account.type) } else { @@ -58,13 +58,13 @@ class ShareFolderPickerController: UITableViewController { } else { cell.accessoryType = .none } - + return cell } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let container = containers?[indexPath.row] else { return } - + if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder { tableView.selectRow(at: nil, animated: false, scrollPosition: .none) } else { @@ -73,5 +73,5 @@ class ShareFolderPickerController: UITableViewController { delegate?.shareFolderPickerDidSelect(container) } } - + } diff --git a/iOS/ShareExtension/ShareViewController.swift b/iOS/ShareExtension/ShareViewController.swift index e81d2772e..425c3748e 100644 --- a/iOS/ShareExtension/ShareViewController.swift +++ b/iOS/ShareExtension/ShareViewController.swift @@ -15,15 +15,15 @@ import RSTree import UniformTypeIdentifiers class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate { - + private var url: URL? private var extensionContainers: ExtensionContainers? private var flattenedContainers: [ExtensionContainer]! private var selectedContainer: ExtensionContainer? private var folderItem: SLComposeSheetConfigurationItem! - + override func viewDidLoad() { - + extensionContainers = ExtensionContainersFile.read() flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]() if let extensionContainers = extensionContainers { @@ -42,7 +42,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont tableView.rowHeight = 38 } - var provider: NSItemProvider? = nil + var provider: NSItemProvider? // Try to get any HTML that is maybe passed in for item in self.extensionContext!.inputItems as! [NSExtensionItem] { @@ -53,7 +53,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont } } - if provider != nil { + if provider != nil { provider!.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] (pList, error) in if error != nil { return @@ -80,7 +80,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont } } - if provider != nil { + if provider != nil { provider!.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil, completionHandler: { [weak self] (urlCoded, error) in if error != nil { return @@ -92,32 +92,32 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont return }) } - + // Reddit in particular doesn't pass the URL correctly and instead puts it in the contentText url = URL(string: contentText) } - + override func isContentValid() -> Bool { return url != nil && selectedContainer != nil } - + override func didSelectPost() { guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else { self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) return } - var name: String? = nil + var name: String? if !contentText.mayBeURL { name = contentText.isEmpty ? nil : contentText } - + let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID) ExtensionFeedAddRequestFile.save(request) - + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } - + func shareFolderPickerDidSelect(_ container: ExtensionContainer) { ShareDefaultContainer.saveDefaultContainer(container) self.selectedContainer = container @@ -126,37 +126,37 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont } override func configurationItems() -> [Any]! { - + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. guard let urlItem = SLComposeSheetConfigurationItem() else { return nil } urlItem.title = "URL" urlItem.value = url?.absoluteString ?? "" - + folderItem = SLComposeSheetConfigurationItem() folderItem.title = "Folder" updateFolderItemValue() - + folderItem.tapHandler = { - + let folderPickerController = ShareFolderPickerController() - + folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder") folderPickerController.delegate = self folderPickerController.containers = self.flattenedContainers folderPickerController.selectedContainerID = self.selectedContainer?.containerID - + self.pushConfigurationViewController(folderPickerController) - + } - + return [folderItem!, urlItem] - + } - + } private extension ShareViewController { - + func updateFolderItemValue() { if let account = selectedContainer as? ExtensionAccount { self.folderItem.value = account.name @@ -164,5 +164,5 @@ private extension ShareViewController { self.folderItem.value = "\(folder.accountName) / \(folder.name)" } } - + } diff --git a/iOS/TitleActivityItemSource.swift b/iOS/TitleActivityItemSource.swift index fff0609aa..a0e233112 100644 --- a/iOS/TitleActivityItemSource.swift +++ b/iOS/TitleActivityItemSource.swift @@ -37,5 +37,5 @@ class TitleActivityItemSource: NSObject, UIActivityItemSource { return NSNull() } } - + } diff --git a/iOS/UIKit Extensions/Animations.swift b/iOS/UIKit Extensions/Animations.swift index 258b173c3..18b256f7a 100644 --- a/iOS/UIKit Extensions/Animations.swift +++ b/iOS/UIKit Extensions/Animations.swift @@ -10,16 +10,16 @@ import Foundation /// Used to select which animations should be performed public struct Animations: OptionSet { - + /// Selections and deselections will be animated. public static let select = Animations(rawValue: 1) - + /// Scrolling will be animated public static let scroll = Animations(rawValue: 2) - + /// Pushing and popping navigation view controllers will be animated public static let navigation = Animations(rawValue: 4) - + public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue diff --git a/iOS/UIKit Extensions/Array-Extensions.swift b/iOS/UIKit Extensions/Array-Extensions.swift index b280bc450..fd724c671 100644 --- a/iOS/UIKit Extensions/Array-Extensions.swift +++ b/iOS/UIKit Extensions/Array-Extensions.swift @@ -9,14 +9,14 @@ import UIKit extension Array where Element == CGRect { - + func maxY() -> CGFloat { - + var y: CGFloat = 0.0 for oneRect in self { y = Swift.max(y, oneRect.maxY) } return y } - + } diff --git a/iOS/UIKit Extensions/Bundle-Extensions.swift b/iOS/UIKit Extensions/Bundle-Extensions.swift index 54b955620..533965428 100644 --- a/iOS/UIKit Extensions/Bundle-Extensions.swift +++ b/iOS/UIKit Extensions/Bundle-Extensions.swift @@ -9,17 +9,17 @@ import Foundation extension Bundle { - + var appName: String { return infoDictionary?["CFBundleName"] as! String } - + var versionNumber: String { return infoDictionary?["CFBundleShortVersionString"] as! String } - + var buildNumber: String { return infoDictionary?["CFBundleVersion"] as! String } - + } diff --git a/iOS/UIKit Extensions/CroppingPreviewParameters.swift b/iOS/UIKit Extensions/CroppingPreviewParameters.swift index 330ade196..582e7a5af 100644 --- a/iOS/UIKit Extensions/CroppingPreviewParameters.swift +++ b/iOS/UIKit Extensions/CroppingPreviewParameters.swift @@ -9,11 +9,11 @@ import UIKit class CroppingPreviewParameters: UIPreviewParameters { - + override init() { super.init() } - + init(view: UIView) { super.init() let newBounds = CGRect(x: 1, y: 1, width: view.bounds.width - 2, height: view.bounds.height - 2) diff --git a/iOS/UIKit Extensions/ImageHeaderView.swift b/iOS/UIKit Extensions/ImageHeaderView.swift index 2e8a25420..d4cc316a3 100644 --- a/iOS/UIKit Extensions/ImageHeaderView.swift +++ b/iOS/UIKit Extensions/ImageHeaderView.swift @@ -11,19 +11,19 @@ import UIKit class ImageHeaderView: UITableViewHeaderFooterView { static let rowHeight = CGFloat(integerLiteral: 88) - + var imageView = UIImageView() - + override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) commonInit() } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + func commonInit() { imageView.tintColor = UIColor.label imageView.contentMode = .scaleAspectFit diff --git a/iOS/UIKit Extensions/InteractiveLabel.swift b/iOS/UIKit Extensions/InteractiveLabel.swift index 989412278..6b04dbe62 100644 --- a/iOS/UIKit Extensions/InteractiveLabel.swift +++ b/iOS/UIKit Extensions/InteractiveLabel.swift @@ -59,10 +59,9 @@ class InteractiveLabel: UILabel, UIEditMenuInteractionDelegate { func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? { - let copyAction = UIAction(title: "Copy", image: nil) { [weak self] action in + let copyAction = UIAction(title: "Copy", image: nil) { [weak self] _ in self?.copy(nil) } return UIMenu(title: "", children: [copyAction]) } } - diff --git a/iOS/UIKit Extensions/InteractiveNavigationController.swift b/iOS/UIKit Extensions/InteractiveNavigationController.swift index 95ec8a304..b42c32a1c 100644 --- a/iOS/UIKit Extensions/InteractiveNavigationController.swift +++ b/iOS/UIKit Extensions/InteractiveNavigationController.swift @@ -9,7 +9,7 @@ import UIKit class InteractiveNavigationController: UINavigationController { - + private let poppableDelegate = PoppableGestureRecognizerDelegate() static func template() -> UINavigationController { @@ -17,13 +17,13 @@ class InteractiveNavigationController: UINavigationController { navController.configure() return navController } - + static func template(rootViewController: UIViewController) -> UINavigationController { let navController = InteractiveNavigationController(rootViewController: rootViewController) navController.configure() return navController } - + override func viewDidLoad() { super.viewDidLoad() poppableDelegate.navigationController = self @@ -40,21 +40,21 @@ class InteractiveNavigationController: UINavigationController { // MARK: Private private extension InteractiveNavigationController { - + func configure() { isToolbarHidden = false - + let navigationStandardAppearance = UINavigationBarAppearance() navigationStandardAppearance.titleTextAttributes = [.foregroundColor: UIColor.label] navigationStandardAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.label] navigationBar.standardAppearance = navigationStandardAppearance - + let scrollEdgeStandardAppearance = UINavigationBarAppearance() scrollEdgeStandardAppearance.backgroundColor = .systemBackground navigationBar.scrollEdgeAppearance = scrollEdgeStandardAppearance - + navigationBar.tintColor = AppAssets.primaryAccentColor - + let toolbarAppearance = UIToolbarAppearance() toolbar.standardAppearance = toolbarAppearance toolbar.compactAppearance = toolbarAppearance diff --git a/iOS/UIKit Extensions/ModalNavigationController.swift b/iOS/UIKit Extensions/ModalNavigationController.swift index cbc7750ba..9d9205095 100644 --- a/iOS/UIKit Extensions/ModalNavigationController.swift +++ b/iOS/UIKit Extensions/ModalNavigationController.swift @@ -12,10 +12,10 @@ class ModalNavigationController: UINavigationController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - + // This hack is to resolve https://github.com/brentsimmons/NetNewsWire/issues/1301 let frame = navigationBar.frame navigationBar.frame = CGRect(x: frame.minX, y: frame.minY, width: frame.size.width, height: 64.0) } - + } diff --git a/iOS/UIKit Extensions/NonIntrinsicLabel.swift b/iOS/UIKit Extensions/NonIntrinsicLabel.swift index 3cd615f6f..60dced0d3 100644 --- a/iOS/UIKit Extensions/NonIntrinsicLabel.swift +++ b/iOS/UIKit Extensions/NonIntrinsicLabel.swift @@ -14,5 +14,5 @@ class NonIntrinsicLabel: UILabel { override var intrinsicContentSize: CGSize { return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) } - + } diff --git a/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift b/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift index 2906290bd..5356944de 100644 --- a/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift +++ b/iOS/UIKit Extensions/PoppableGestureRecognizerDelegate.swift @@ -10,7 +10,7 @@ import UIKit final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { - + weak var navigationController: UINavigationController? func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -20,12 +20,12 @@ final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDele func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } - + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if otherGestureRecognizer is UIPanGestureRecognizer { return true } return false } - + } diff --git a/iOS/UIKit Extensions/String-Extensions.swift b/iOS/UIKit Extensions/String-Extensions.swift index 538cd92eb..a1a4bf6c9 100644 --- a/iOS/UIKit Extensions/String-Extensions.swift +++ b/iOS/UIKit Extensions/String-Extensions.swift @@ -9,17 +9,17 @@ import UIKit extension String { - + func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat { let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) return ceil(boundingBox.height) } - + func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) return ceil(boundingBox.width) } - + } diff --git a/iOS/UIKit Extensions/TickMarkSlider.swift b/iOS/UIKit Extensions/TickMarkSlider.swift index 5a7aa73ba..a96d480c4 100644 --- a/iOS/UIKit Extensions/TickMarkSlider.swift +++ b/iOS/UIKit Extensions/TickMarkSlider.swift @@ -12,7 +12,7 @@ class TickMarkSlider: UISlider { private var enableFeedback = false private let feedbackGenerator = UISelectionFeedbackGenerator() - + private var roundedValue: Float? override var value: Float { didSet { @@ -23,17 +23,17 @@ class TickMarkSlider: UISlider { } } } - + func addTickMarks() { enableFeedback = true - + let numberOfGaps = Int(maximumValue) - Int(minimumValue) - + var gapLayoutGuides = [UILayoutGuide]() - + for i in 0...numberOfGaps { - + let tick = UIView() tick.translatesAutoresizingMaskIntoConstraints = false tick.backgroundColor = AppAssets.tickMarkColor @@ -46,11 +46,11 @@ class TickMarkSlider: UISlider { if i == 0 { tick.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true } - + if let lastGapLayoutGuild = gapLayoutGuides.last { lastGapLayoutGuild.trailingAnchor.constraint(equalTo: tick.leadingAnchor).isActive = true } - + if i != numberOfGaps { let gapLayoutGuild = UILayoutGuide() gapLayoutGuides.append(gapLayoutGuild) @@ -59,17 +59,17 @@ class TickMarkSlider: UISlider { } else { tick.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true } - + } - + if let firstGapLayoutGuild = gapLayoutGuides.first { for i in 1.. Bool { let result = super.continueTracking(touch, with: event) value = value.rounded() @@ -79,5 +79,5 @@ class TickMarkSlider: UISlider { override func endTracking(_ touch: UITouch?, with event: UIEvent?) { value = value.rounded() } - + } diff --git a/iOS/UIKit Extensions/UIActivityViewController-Extensions.swift b/iOS/UIKit Extensions/UIActivityViewController-Extensions.swift index 453fb64be..12d11b24d 100644 --- a/iOS/UIKit Extensions/UIActivityViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIActivityViewController-Extensions.swift @@ -12,7 +12,7 @@ extension UIActivityViewController { convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) { let itemSource = ArticleActivityItemSource(url: url, subject: title) let titleSource = TitleActivityItemSource(title: title) - + self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities) } } diff --git a/iOS/UIKit Extensions/UIBarButtonItem-Extensions.swift b/iOS/UIKit Extensions/UIBarButtonItem-Extensions.swift index b05e77fdd..abdd086a9 100644 --- a/iOS/UIKit Extensions/UIBarButtonItem-Extensions.swift +++ b/iOS/UIKit Extensions/UIBarButtonItem-Extensions.swift @@ -9,7 +9,7 @@ import UIKit public extension UIBarButtonItem { - + @IBInspectable var accEnabled: Bool { get { return isAccessibilityElement @@ -18,7 +18,7 @@ public extension UIBarButtonItem { isAccessibilityElement = newValue } } - + @IBInspectable var accLabelText: String? { get { return accessibilityLabel @@ -27,5 +27,5 @@ public extension UIBarButtonItem { accessibilityLabel = newValue } } - + } diff --git a/iOS/UIKit Extensions/UIFont-Extensions.swift b/iOS/UIKit Extensions/UIFont-Extensions.swift index d250066b8..cffaa400c 100644 --- a/iOS/UIKit Extensions/UIFont-Extensions.swift +++ b/iOS/UIKit Extensions/UIFont-Extensions.swift @@ -9,21 +9,21 @@ import UIKit extension UIFont { - - func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont { + + func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont { if let descriptor = fontDescriptor.withSymbolicTraits(traits) { - return UIFont(descriptor: descriptor, size: 0) //size 0 means keep the size as it is + return UIFont(descriptor: descriptor, size: 0) // size 0 means keep the size as it is } else { return self } } - + func bold() -> UIFont { return withTraits(traits: .traitBold) } - + func italic() -> UIFont { return withTraits(traits: .traitItalic) } - + } diff --git a/iOS/UIKit Extensions/UIPageViewController-Extensions.swift b/iOS/UIKit Extensions/UIPageViewController-Extensions.swift index 8db0ce553..adfa5135d 100644 --- a/iOS/UIKit Extensions/UIPageViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIPageViewController-Extensions.swift @@ -9,7 +9,7 @@ import UIKit extension UIPageViewController { - + var scrollViewInsidePageControl: UIScrollView? { for view in view.subviews { if let scrollView = view as? UIScrollView { @@ -18,5 +18,5 @@ extension UIPageViewController { } return nil } - + } diff --git a/iOS/UIKit Extensions/UIStoryboard-Extensions.swift b/iOS/UIKit Extensions/UIStoryboard-Extensions.swift index ef48e061d..0c46b47e9 100644 --- a/iOS/UIKit Extensions/UIStoryboard-Extensions.swift +++ b/iOS/UIKit Extensions/UIStoryboard-Extensions.swift @@ -9,39 +9,39 @@ import UIKit extension UIStoryboard { - + static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) - + static var main: UIStoryboard { return UIStoryboard(name: "Main", bundle: nil) } - + static var add: UIStoryboard { return UIStoryboard(name: "Add", bundle: nil) } - + static var settings: UIStoryboard { return UIStoryboard(name: "Settings", bundle: nil) } - + static var inspector: UIStoryboard { return UIStoryboard(name: "Inspector", bundle: nil) } - + static var account: UIStoryboard { return UIStoryboard(name: "Account", bundle: nil) } - + func instantiateController(ofType type: T.Type = T.self) -> T where T: UIViewController { - + let storyboardId = String(describing: type) guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else { print("Unable to load view with Scene Identifier: \(storyboardId)") fatalError() } - + return viewController - + } - + } diff --git a/iOS/UIKit Extensions/UITableView-Extensions.swift b/iOS/UIKit Extensions/UITableView-Extensions.swift index fd0c4c0ee..532a339c0 100644 --- a/iOS/UIKit Extensions/UITableView-Extensions.swift +++ b/iOS/UIKit Extensions/UITableView-Extensions.swift @@ -9,7 +9,7 @@ import UIKit extension UITableView { - + /** Selects a row and scrolls it to the middle if it is not visible */ @@ -20,7 +20,7 @@ extension UITableView { indexPath.row < dataSource.tableView(self, numberOfRowsInSection: indexPath.section) else { return } - + selectRow(at: indexPath, animated: animations.contains(.select), scrollPosition: .none) if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame) { @@ -29,12 +29,12 @@ extension UITableView { } } } - + func cellCompletelyVisible(_ indexPath: IndexPath) -> Bool { let rect = rectForRow(at: indexPath) return safeAreaLayoutGuide.layoutFrame.contains(rect) } - + public func middleVisibleRow() -> IndexPath? { if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame), visibleIndexPaths.count > 2 { return visibleIndexPaths[visibleIndexPaths.count / 2] diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 1ed607502..cd1c49ae8 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -11,7 +11,7 @@ import RSCore import Account extension UIViewController { - + func presentError(_ error: Error, dismiss: (() -> Void)? = nil) { if let accountError = error as? AccountError, accountError.isCredentialsError { presentAccountError(accountError, dismiss: dismiss) @@ -41,7 +41,7 @@ extension UIViewController { let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist. %@.", comment: "Decoding key missing") informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String presentError(title: errorTitle, message: informativeText, dismiss: dismiss) - + default: informativeText = error.localizedDescription presentError(title: errorTitle, message: informativeText, dismiss: dismiss) @@ -55,35 +55,35 @@ extension UIViewController { } private extension UIViewController { - + func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) { let title = NSLocalizedString("Account Error", comment: "Account Error") let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) - + if error.account?.type == .feedbin { let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials") let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in dismiss?() - + let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController navController.modalPresentationStyle = .formSheet let addViewController = navController.topViewController as! FeedbinAccountViewController addViewController.account = error.account self?.present(navController, animated: true) } - + alertController.addAction(credentialsAction) alertController.preferredAction = credentialsAction } - + let dismissTitle = NSLocalizedString("OK", comment: "OK") let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in dismiss?() } alertController.addAction(dismissAction) - + self.present(alertController, animated: true, completion: nil) } diff --git a/iOS/UIKit Extensions/VibrantButton.swift b/iOS/UIKit Extensions/VibrantButton.swift index ca2452eaa..f871a50da 100644 --- a/iOS/UIKit Extensions/VibrantButton.swift +++ b/iOS/UIKit Extensions/VibrantButton.swift @@ -9,7 +9,7 @@ import UIKit class VibrantButton: UIButton { - + @IBInspectable var backgroundHighlightColor: UIColor = AppAssets.secondaryAccentColor override init(frame: CGRect) { @@ -20,7 +20,7 @@ class VibrantButton: UIButton { super.init(coder: coder) commonInit() } - + private func commonInit() { setTitleColor(AppAssets.vibrantTextColor, for: .highlighted) let disabledColor = AppAssets.secondaryAccentColor.withAlphaComponent(0.5) @@ -47,5 +47,5 @@ class VibrantButton: UIButton { isHighlighted = false super.touchesCancelled(touches, with: event) } - + } diff --git a/iOS/UIKit Extensions/VibrantLabel.swift b/iOS/UIKit Extensions/VibrantLabel.swift index 9e480a7c4..20c2cd357 100644 --- a/iOS/UIKit Extensions/VibrantLabel.swift +++ b/iOS/UIKit Extensions/VibrantLabel.swift @@ -9,17 +9,17 @@ import UIKit class VibrantLabel: UILabel { - + override init(frame: CGRect) { super.init(frame: frame) commonInit() } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + private func commonInit() { highlightedTextColor = AppAssets.vibrantTextColor } diff --git a/iOS/UIKit Extensions/VibrantTableViewCell.swift b/iOS/UIKit Extensions/VibrantTableViewCell.swift index 22d74deb0..cde421a36 100644 --- a/iOS/UIKit Extensions/VibrantTableViewCell.swift +++ b/iOS/UIKit Extensions/VibrantTableViewCell.swift @@ -9,27 +9,27 @@ import UIKit class VibrantTableViewCell: UITableViewCell { - + static let duration: TimeInterval = 0.6 - + var labelColor: UIColor { return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label } - + var secondaryLabelColor: UIColor { return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.secondaryLabel } - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonInit() } - + required init?(coder: NSCoder) { super.init(coder: coder) commonInit() } - + private func commonInit() { applyThemeProperties() } @@ -43,7 +43,7 @@ class VibrantTableViewCell: UITableViewCell { super.setSelected(selected, animated: animated) updateVibrancy(animated: animated) } - + /// Subclass overrides should call super func applyThemeProperties() { let selectedBackgroundView = UIView(frame: .zero) @@ -56,7 +56,7 @@ class VibrantTableViewCell: UITableViewCell { updateLabelVibrancy(textLabel, color: labelColor, animated: animated) updateLabelVibrancy(detailTextLabel, color: labelColor, animated: animated) } - + func updateLabelVibrancy(_ label: UILabel?, color: UIColor, animated: Bool) { guard let label = label else { return } if animated { @@ -67,33 +67,33 @@ class VibrantTableViewCell: UITableViewCell { label.textColor = color } } - + } class VibrantBasicTableViewCell: VibrantTableViewCell { - + @IBOutlet private var label: UILabel! @IBOutlet private var detail: UILabel! @IBOutlet private var icon: UIImageView! - + @IBInspectable var imageNormal: UIImage? @IBInspectable var imageSelected: UIImage? - + var iconTint: UIColor { return isHighlighted || isSelected ? labelColor : AppAssets.primaryAccentColor } - + var iconImage: UIImage? { return isHighlighted || isSelected ? imageSelected : imageNormal } - + override func updateVibrancy(animated: Bool) { super.updateVibrancy(animated: animated) updateIconVibrancy(icon, color: iconTint, image: iconImage, animated: animated) updateLabelVibrancy(label, color: labelColor, animated: animated) updateLabelVibrancy(detail, color: secondaryLabelColor, animated: animated) } - + private func updateIconVibrancy(_ icon: UIImageView?, color: UIColor, image: UIImage?, animated: Bool) { guard let icon = icon else { return } if animated { @@ -106,5 +106,5 @@ class VibrantBasicTableViewCell: VibrantTableViewCell { icon.image = image } } - + }