Merge pull request #3441 from stuartbreckenridge/ios-ui-notifications

Notifications Manager Performance Improvements
This commit is contained in:
Maurice Parker
2022-02-10 14:05:49 -08:00
committed by GitHub
5 changed files with 223 additions and 45 deletions

View File

@@ -54,6 +54,10 @@ struct AppAssets {
static var accountTheOldReaderImage: UIImage = {
return UIImage(named: "accountTheOldReader")!
}()
static var appBadgeImage: UIImage = {
return UIImage(systemName: "app.badge")!
}()
static var articleExtractorError: UIImage = {
return UIImage(named: "articleExtractorError")!
@@ -181,6 +185,10 @@ struct AppAssets {
return UIImage(systemName: "ellipsis.circle")!
}()
static var moreImageFill: UIImage = {
return UIImage(systemName: "ellipsis.circle.fill")!
}()
static var nextArticleImage: UIImage = {
return UIImage(systemName: "chevron.down")!
}()

View File

@@ -10,11 +10,8 @@ import UIKit
import Account
import UserNotifications
extension Notification.Name {
static let NotificationPreferencesDidUpdate = Notification.Name("NotificationPreferencesDidUpdate")
}
class NotificationsTableViewCell: UITableViewCell {
class NotificationsTableViewCell: VibrantBasicTableViewCell {
@IBOutlet weak var notificationsSwitch: UISwitch!
@IBOutlet weak var notificationsLabel: UILabel!
@@ -33,7 +30,7 @@ class NotificationsTableViewCell: UITableViewCell {
// Configure the view for the selected state
}
func configure(_ webFeed: WebFeed, _ status: UNAuthorizationStatus) {
func configure(_ webFeed: WebFeed) {
self.feed = webFeed
var isOn = false
if webFeed.isNotifyAboutNewArticles == nil {
@@ -43,9 +40,8 @@ class NotificationsTableViewCell: UITableViewCell {
}
notificationsSwitch.isOn = isOn
notificationsSwitch.addTarget(self, action: #selector(toggleWebFeedNotification(_:)), for: .touchUpInside)
if status == .denied { notificationsSwitch.isEnabled = false }
notificationsLabel.text = webFeed.nameForDisplay
notificationsImageView.image = webFeed.smallIcon?.image
notificationsImageView.image = IconImageCache.shared.imageFor(webFeed.feedID!)?.image
notificationsImageView.layer.cornerRadius = 4
}
@@ -61,14 +57,5 @@ class NotificationsTableViewCell: UITableViewCell {
feed.isNotifyAboutNewArticles!.toggle()
}
}
@objc
private func requestNotificationPermissions(_ sender: Any) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
NotificationCenter.default.post(name: .NotificationPreferencesDidUpdate, object: nil)
}
}
}

View File

@@ -11,22 +11,47 @@ import Account
import UserNotifications
class NotificationsViewController: UIViewController {
@IBOutlet weak var notificationsTableView: UITableView!
private lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = NSLocalizedString("Find a feed", comment: "Find a feed")
searchController.searchBar.searchBarStyle = .minimal
searchController.delegate = self
searchController.searchBar.delegate = self
searchController.searchBar.sizeToFit()
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
self.definesPresentationContext = true
return searchController
}()
private var status: UNAuthorizationStatus = .notDetermined
private var newArticleNotificationFilter: Bool = false {
didSet {
filterButton.menu = notificationFilterMenu()
}
}
private var filterButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
self.title = NSLocalizedString("New Article Notifications", comment: "Notifications")
notificationsTableView.sectionHeaderTopPadding = 25
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("New Article Notifications", comment: "Notifications")
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: .NotificationPreferencesDidUpdate, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
notificationsTableView.prefetchDataSource = self
navigationItem.searchController = searchController
filterButton = UIBarButtonItem(
title: nil,
image: AppAssets.moreImage,
primaryAction: nil,
menu: notificationFilterMenu())
navigationItem.rightBarButtonItem = filterButton
reloadNotificationTableView()
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
}
@objc
@@ -34,18 +59,114 @@ class NotificationsViewController: UIViewController {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.status = settings.authorizationStatus
if self.status != .authorized {
self.filterButton.isEnabled = false
self.newArticleNotificationFilter = false
}
self.notificationsTableView.reloadData()
}
}
}
private func notificationFilterMenu() -> UIMenu {
if filterButton != nil {
if newArticleNotificationFilter {
filterButton.image = AppAssets.moreImageFill
} else {
filterButton.image = AppAssets.moreImage
}
}
let filterMenu = UIMenu(title: "",
image: nil,
identifier: nil,
options: [.displayInline],
children: [
UIAction(
title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: newArticleNotificationFilter ? .on : .off,
handler: { [weak self] _ in
self?.newArticleNotificationFilter.toggle()
self?.notificationsTableView.reloadData()
})])
var menus = [UIMenuElement]()
menus.append(filterMenu)
for account in AccountManager.shared.sortedActiveAccounts {
let accountMenu = UIMenu(title: account.nameForDisplay, image: nil, identifier: nil, options: .singleSelection, children: [enableAllAction(for: account), disableAllAction(for: account)])
menus.append(accountMenu)
}
let combinedMenu = UIMenu(title: "",
image: nil,
identifier: nil,
options: .displayInline,
children: menus)
return combinedMenu
}
private func enableAllAction(for account: Account) -> UIAction {
let action = UIAction(title: NSLocalizedString("Enable All Notifications", comment: "Enable All"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { [weak self] _ in
for feed in account.flattenedWebFeeds() {
feed.isNotifyAboutNewArticles = true
}
self?.notificationsTableView.reloadData()
self?.filterButton.menu = self?.notificationFilterMenu()
}
return action
}
private func disableAllAction(for account: Account) -> UIAction {
let action = UIAction(title: NSLocalizedString("Disable All Notifications", comment: "Disable All"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { [weak self] _ in
for feed in account.flattenedWebFeeds() {
feed.isNotifyAboutNewArticles = false
}
self?.notificationsTableView.reloadData()
self?.filterButton.menu = self?.notificationFilterMenu()
}
return action
}
// MARK: - Feed Filtering
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
}
private func filteredWebFeeds(_ searchText: String? = "", account: Account) -> [WebFeed] {
sortedWebFeedsForAccount(account).filter { feed in
return feed.nameForDisplay.lowercased().contains(searchText!.lowercased())
}
}
private func feedsWithNotificationsEnabled(_ account: Account) -> [WebFeed] {
sortedWebFeedsForAccount(account).filter { feed in
return feed.isNotifyAboutNewArticles == true
}
}
}
// MARK: UITableViewDataSource
// MARK: - UITableViewDataSource
extension NotificationsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
@@ -58,25 +179,40 @@ extension NotificationsViewController: UITableViewDataSource {
if status == .denied { return 1 }
return 0
}
return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count
if searchController.isActive {
return filteredWebFeeds(searchController.searchBar.text, account: AccountManager.shared.sortedActiveAccounts[section - 1]).count
} else if newArticleNotificationFilter == true {
return feedsWithNotificationsEnabled(AccountManager.shared.sortedActiveAccounts[section - 1]).count
} else {
return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let openSettingsCell = tableView.dequeueReusableCell(withIdentifier: "OpenSettingsCell") as! VibrantBasicTableViewCell
return openSettingsCell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
let feed = sortedWebFeedsForAccount(account)[indexPath.row]
cell.configure(feed, status)
return cell
if searchController.isActive {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(filteredWebFeeds(searchController.searchBar.text, account: account)[indexPath.row])
return cell
} else if newArticleNotificationFilter == true {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(feedsWithNotificationsEnabled(account)[indexPath.row])
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(sortedWebFeedsForAccount(account)[indexPath.row])
return cell
}
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 { return nil }
return AccountManager.shared.sortedActiveAccounts[section - 1].nameForDisplay
@@ -92,12 +228,55 @@ extension NotificationsViewController: UITableViewDataSource {
}
}
// MARK: - UITableViewDelegate
extension NotificationsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
if indexPath.section == 0 {
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
}
}
}
extension NotificationsViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
for path in indexPaths {
let account = AccountManager.shared.sortedActiveAccounts[path.section - 1]
let feed = sortedWebFeedsForAccount(account)[path.row]
let _ = IconImageCache.shared.imageFor(feed.feedID!)
}
}
}
// MARK: - UISearchControllerDelegate
extension NotificationsViewController: UISearchControllerDelegate {
func didDismissSearchController(_ searchController: UISearchController) {
print(#function)
searchController.isActive = false
notificationsTableView.reloadData()
}
}
// MARK: - UISearchBarDelegate
extension NotificationsViewController: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchController.isActive = true
newArticleNotificationFilter = false
notificationsTableView.reloadData()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
notificationsTableView.reloadData()
}
}

View File

@@ -43,8 +43,8 @@
<rect key="frame" x="0.0" y="0.0" width="345.5" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Notifications" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="l7X-8L-61m">
<rect key="frame" x="20" y="0.0" width="317.5" height="44"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="New Article Notifications" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" id="l7X-8L-61m">
<rect key="frame" x="20" y="0.0" width="317.5" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
@@ -1068,7 +1068,7 @@
<!--Notifications View Controller-->
<scene sceneID="Brh-C7-mfB">
<objects>
<viewController storyboardIdentifier="NotificationsViewController" id="vak-Sx-5aB" customClass="NotificationsViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<viewController storyboardIdentifier="NotificationsViewController" extendedLayoutIncludesOpaqueBars="YES" modalPresentationStyle="overCurrentContext" id="vak-Sx-5aB" customClass="NotificationsViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="2EC-mY-aSE">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -1147,14 +1147,15 @@
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="32c-qn-nBM"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="backgroundColor" systemColor="secondarySystemGroupedBackgroundColor"/>
<constraints>
<constraint firstItem="dsQ-vy-iVG" firstAttribute="top" secondItem="32c-qn-nBM" secondAttribute="top" constant="-88" id="3o7-Oa-Qhq"/>
<constraint firstItem="dsQ-vy-iVG" firstAttribute="top" secondItem="2EC-mY-aSE" secondAttribute="top" id="3o7-Oa-Qhq"/>
<constraint firstItem="dsQ-vy-iVG" firstAttribute="leading" secondItem="32c-qn-nBM" secondAttribute="leading" id="V01-Fb-OvO"/>
<constraint firstItem="32c-qn-nBM" firstAttribute="trailing" secondItem="dsQ-vy-iVG" secondAttribute="trailing" id="WTV-jU-eAU"/>
<constraint firstItem="dsQ-vy-iVG" firstAttribute="bottom" secondItem="32c-qn-nBM" secondAttribute="bottom" constant="34" id="nQh-bT-XE2"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="sdU-Jy-5dq"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<connections>
<outlet property="notificationsTableView" destination="dsQ-vy-iVG" id="Maw-PU-kb4"/>
@@ -1329,6 +1330,9 @@
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="secondarySystemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>

View File

@@ -47,6 +47,8 @@ class SettingsViewController: UITableViewController {
tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell")
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
refreshNotificationStatus()
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44
}
@@ -72,7 +74,6 @@ class SettingsViewController: UITableViewController {
refreshClearsReadArticlesSwitch.isOn = false
}
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
if AppDefaults.shared.confirmMarkAllAsRead {
@@ -106,7 +107,6 @@ class SettingsViewController: UITableViewController {
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
scrollToArticlesSection = false
}
refreshNotificationStatus()
}
@objc