NetNewsWire/iOS/AppDelegate.swift

340 lines
11 KiB
Swift
Raw Normal View History

//
// AppDelegate.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
2025-01-03 11:48:19 -08:00
import os
import RSCore
import Account
import Articles
@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
2025-01-22 22:18:09 -08:00
2025-02-01 16:25:54 -08:00
var window: UIWindow?
2025-01-22 22:18:09 -08:00
private var coordinator: SceneCoordinator?
2025-01-03 11:48:19 -08:00
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
private var unreadCount = 0 {
didSet {
if unreadCount != oldValue {
UNUserNotificationCenter.current().setBadgeCount(unreadCount)
}
}
}
2025-01-22 22:18:09 -08:00
// MARK: - Lifecycle
2025-02-01 17:34:45 -08:00
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
2025-02-01 17:34:45 -08:00
AppDefaults.registerDefaults()
let isFirstRun = AppDefaults.isFirstRun
if isFirstRun {
2025-01-03 11:48:19 -08:00
logger.info("Is first run.")
}
2025-01-22 22:18:09 -08:00
_ = AccountManager.shared
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: AccountManager.shared)
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidTriggerManualRefresh(_:)), name: .userDidTriggerManualRefresh, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
2025-02-01 21:38:43 -08:00
DefaultFeedsImporter.importDefaultFeeds(account: AccountManager.shared.defaultAccount)
}
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
BackgroundTaskManager.shared.delegate = self
BackgroundTaskManager.shared.registerTasks()
CacheCleaner.purgeIfNecessary()
2025-02-01 21:38:43 -08:00
addHomeScreenQuickActions()
2025-01-22 22:18:09 -08:00
2025-02-01 20:36:18 -08:00
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { granted, _ in
guard granted else { return }
Task { @MainActor in
UIApplication.shared.registerForRemoteNotifications()
2019-04-23 07:48:22 -05:00
}
}
UNUserNotificationCenter.current().delegate = self
2025-02-01 21:38:43 -08:00
_ = ArticleThemesManager.shared
2025-02-01 20:06:58 -08:00
_ = UserNotificationManager.shared
2025-02-01 20:09:34 -08:00
_ = ExtensionContainersFile.shared
_ = ExtensionFeedAddRequestFile.shared
_ = WidgetDataEncoder.shared
2025-02-01 20:03:01 -08:00
_ = ArticleStatusSyncTimer.shared
2025-02-01 20:28:37 -08:00
_ = FaviconDownloader.shared
_ = FeedIconDownloader.shared
2025-02-01 21:38:43 -08:00
#if DEBUG
2025-02-01 20:03:01 -08:00
ArticleStatusSyncTimer.shared.update()
2025-02-01 21:38:43 -08:00
#endif
2025-01-22 22:18:09 -08:00
2025-02-01 17:34:45 -08:00
// Create window.
2025-02-01 16:25:54 -08:00
let window = UIWindow(frame: UIScreen.main.bounds)
self.window = window
2025-02-01 17:34:45 -08:00
// Create UI and add it to window.
2025-02-01 16:25:54 -08:00
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let rootSplitViewController = storyboard.instantiateInitialViewController() as! RootSplitViewController
2025-02-01 17:34:45 -08:00
rootSplitViewController.presentsWithGesture = true
rootSplitViewController.showsSecondaryOnlyButton = true
rootSplitViewController.preferredDisplayMode = .oneBesideSecondary
coordinator = SceneCoordinator(rootSplitViewController: rootSplitViewController)
rootSplitViewController.coordinator = coordinator
rootSplitViewController.delegate = coordinator
2025-02-01 16:25:54 -08:00
window.rootViewController = rootSplitViewController
2025-02-01 17:34:45 -08:00
window.tintColor = AppColor.accent
updateUserInterfaceStyle()
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
2025-02-01 16:25:54 -08:00
window.makeKeyAndVisible()
2025-01-22 22:18:09 -08:00
2025-02-01 17:34:45 -08:00
Task { @MainActor in
// Ensure Feeds view shows on first run on iPad  otherwise the UI is empty.
if UIDevice.current.userInterfaceIdiom == .pad && AppDefaults.isFirstRun {
rootSplitViewController.show(.primary)
2025-02-01 17:34:45 -08:00
}
2025-02-01 20:36:18 -08:00
self.unreadCount = AccountManager.shared.unreadCount
2025-02-01 17:34:45 -08:00
}
2025-02-01 16:25:54 -08:00
return true
}
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.main.async {
AccountManager.shared.resumeAllIfSuspended()
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
self.suspendApplication()
completionHandler(.newData)
}
}
2025-02-01 21:38:43 -08:00
}
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
func applicationWillEnterForeground(_ application: UIApplication) {
prepareAccountsForForeground()
coordinator?.resetFocus()
}
private func prepareAccountsForForeground() {
AccountManager.shared.resumeAllIfSuspended()
ExtensionFeedAddRequestFile.shared.resume()
ArticleStatusSyncTimer.shared.update()
if let lastRefresh = AppDefaults.lastRefresh {
if Date() > lastRefresh.addingTimeInterval(15 * 60) {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
} else {
AccountManager.shared.syncArticleStatusAll()
}
} else {
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
}
2019-06-28 10:28:02 -05:00
}
func applicationDidEnterBackground(_ application: UIApplication) {
IconImageCache.shared.emptyCache()
2025-02-01 21:38:43 -08:00
ArticleStringFormatter.emptyCaches()
prepareAccountsForBackground()
}
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
private func prepareAccountsForBackground() {
ExtensionFeedAddRequestFile.shared.suspend()
ArticleStatusSyncTimer.shared.invalidate()
BackgroundTaskManager.shared.scheduleBackgroundFeedRefresh()
BackgroundTaskManager.shared.syncArticleStatus()
WidgetDataEncoder.shared.encode()
BackgroundTaskManager.shared.waitForSyncTasksToFinish()
}
func applicationWillTerminate(_ application: UIApplication) {
ArticleStatusSyncTimer.shared.stop()
}
private func suspendApplication() {
guard UIApplication.shared.applicationState == .background else { return }
AccountManager.shared.suspendNetworkAll()
AccountManager.shared.suspendDatabaseAll()
ArticleThemeDownloader.shared.cleanUp()
CoalescingQueue.standard.performCallsImmediately()
coordinator?.suspend()
logger.info("Application processing suspended.")
}
}
// MARK: - Notifications
extension AppDelegate {
2025-01-22 22:18:09 -08:00
2019-06-28 10:28:02 -05:00
@objc func unreadCountDidChange(_ note: Notification) {
2025-02-01 20:28:37 -08:00
assert(Thread.isMainThread)
assert(note.object is AccountManager)
unreadCount = AccountManager.shared.unreadCount
}
2025-01-22 22:18:09 -08:00
2019-06-28 10:28:02 -05:00
@objc func accountRefreshDidFinish(_ note: Notification) {
AppDefaults.lastRefresh = Date()
2019-06-28 10:28:02 -05:00
}
2025-01-22 22:18:09 -08:00
@objc func userDidTriggerManualRefresh(_ note: Notification) {
2025-02-01 21:38:43 -08:00
assert(Thread.isMainThread)
guard let errorHandler = note.userInfo?[UserInfoKey.errorHandler] as? ErrorHandlerBlock else {
assertionFailure("Expected errorHandler in .userDidTriggerManualRefresh userInfo")
return
}
coordinator?.cleanUp(conditional: true)
AccountManager.shared.refreshAll(errorHandler: errorHandler)
}
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
@objc func userDefaultsDidChange(_ note: Notification) {
updateUserInterfaceStyle()
}
2025-02-01 21:38:43 -08:00
}
2025-01-03 11:48:19 -08:00
2025-02-01 21:38:43 -08:00
// MARK: - UNUserNotificationCenterDelegate
2025-02-01 21:38:43 -08:00
extension AppDelegate {
2025-01-22 22:18:09 -08:00
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.list, .banner, .badge, .sound])
}
2025-01-22 22:18:09 -08:00
2025-02-01 17:48:47 -08:00
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
2019-10-03 09:53:21 -05:00
defer { completionHandler() }
2025-01-22 22:18:09 -08:00
2020-12-23 20:15:25 +08:00
let userInfo = response.notification.request.content.userInfo
2025-01-22 22:18:09 -08:00
2020-12-23 20:15:25 +08:00
switch response.actionIdentifier {
case "MARK_AS_READ":
handleMarkAsRead(userInfo: userInfo)
case "MARK_AS_STARRED":
handleMarkAsStarred(userInfo: userInfo)
default:
2025-02-01 17:48:47 -08:00
handle(response)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.coordinator?.dismissIfLaunchingFromExternalAction()
2020-12-23 20:15:25 +08:00
}
2019-10-03 09:53:21 -05:00
}
2025-02-01 17:48:47 -08:00
}
}
2025-02-01 21:38:43 -08:00
// MARK: - Home Screen Quick Actions
2019-09-01 16:54:07 -05:00
private extension AppDelegate {
2025-01-22 22:18:09 -08:00
2025-02-01 20:33:50 -08:00
enum ShortcutItemType: String {
case firstUnread = "com.ranchero.NetNewsWire.FirstUnread"
case showSearch = "com.ranchero.NetNewsWire.ShowSearch"
case addFeed = "com.ranchero.NetNewsWire.ShowAdd"
}
2025-02-01 21:38:43 -08:00
private func addHomeScreenQuickActions() {
2019-09-01 16:54:07 -05:00
let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle")
2025-02-01 20:33:50 -08:00
let unreadItem = UIApplicationShortcutItem(type: ShortcutItemType.firstUnread.rawValue, localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
2025-01-22 22:18:09 -08:00
2019-09-01 16:54:07 -05:00
let searchTitle = NSLocalizedString("Search", comment: "Search")
let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
2025-02-01 20:33:50 -08:00
let searchItem = UIApplicationShortcutItem(type: ShortcutItemType.showSearch.rawValue, localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil)
2019-09-01 16:54:07 -05:00
2019-09-02 15:45:09 -05:00
let addTitle = NSLocalizedString("Add Feed", comment: "Add Feed")
let addIcon = UIApplicationShortcutIcon(systemImageName: "plus")
2025-02-01 20:33:50 -08:00
let addItem = UIApplicationShortcutItem(type: ShortcutItemType.addFeed.rawValue, localizedTitle: addTitle, localizedSubtitle: nil, icon: addIcon, userInfo: nil)
2019-09-02 16:05:55 -05:00
UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem]
2019-09-01 16:54:07 -05:00
}
}
2025-02-01 17:34:45 -08:00
// MARK: - Private
private extension AppDelegate {
func updateUserInterfaceStyle() {
assert(Thread.isMainThread)
guard let window else {
// Could be nil legitimately  this can get called before window is set up.
2025-02-01 17:34:45 -08:00
return
}
let updatedStyle = AppDefaults.userInterfaceColorPalette.uiUserInterfaceStyle
if window.overrideUserInterfaceStyle != updatedStyle {
window.overrideUserInterfaceStyle = updatedStyle
}
}
}
2025-02-01 21:38:43 -08:00
// MARK: - BackgroundTaskManagerDelegate
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
extension AppDelegate: BackgroundTaskManagerDelegate {
2025-01-22 22:18:09 -08:00
2025-02-01 21:38:43 -08:00
func backgroundTaskManagerApplicationShouldSuspend(_: BackgroundTaskManager) {
suspendApplication()
2019-06-19 23:26:03 +08:00
}
}
2020-12-23 20:15:25 +08:00
// MARK: - Handle Notification Actions
2020-12-23 20:15:25 +08:00
private extension AppDelegate {
2025-01-22 22:18:09 -08:00
2020-12-23 20:15:25 +08:00
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
handleMarked(userInfo: userInfo, statusKey: .read)
2020-12-23 20:15:25 +08:00
}
2025-01-22 22:18:09 -08:00
2020-12-23 20:15:25 +08:00
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
handleMarked(userInfo: userInfo, statusKey: .starred)
}
func handleMarked(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
2025-01-22 22:18:09 -08:00
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
2020-12-23 20:15:25 +08:00
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return
}
AccountManager.shared.resumeAllIfSuspended()
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
logger.debug("No account found from notification with accountID \(accountID).")
2020-12-23 20:15:25 +08:00
return
}
guard let article = try? account.fetchArticles(.articleIDs([articleID])) else {
logger.debug("No articles found from search using \(articleID)")
2020-12-23 20:15:25 +08:00
return
}
account.markArticles(article, statusKey: statusKey, flag: true) { _ in }
prepareAccountsForBackground()
account.syncArticleStatus { _ in
2020-12-23 21:16:32 +08:00
if !AccountManager.shared.isSuspended {
self.prepareAccountsForBackground()
self.suspendApplication()
2020-12-23 20:15:25 +08:00
}
}
2020-12-23 20:15:25 +08:00
}
2025-02-01 17:48:47 -08:00
func handle(_ response: UNNotificationResponse) {
AccountManager.shared.resumeAllIfSuspended()
coordinator?.handle(response)
2025-02-01 17:48:47 -08:00
}
2020-12-23 20:15:25 +08:00
}