From 0c9a1ba8d0652b605cca0db33589313333bc643d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 3 Oct 2019 09:53:21 -0500 Subject: [PATCH] Add notification deep linking for iOS --- .../Account/Account.xcodeproj/project.pbxproj | 4 ++ Frameworks/Account/DeepLinkProvider.swift | 21 ++++++++++ Frameworks/Account/Feed.swift | 11 ++++- Frameworks/Account/Folder.swift | 11 ++++- NetNewsWire.xcodeproj/project.pbxproj | 4 -- Shared/Activity/ActivityID.swift | 17 -------- Shared/Activity/ActivityManager.swift | 23 ++--------- Shared/Data/ArticleUtilities.swift | 15 +++++++ .../UserNotificationManager.swift | 11 +++-- iOS/AppDelegate.swift | 9 +++++ iOS/SceneCoordinator.swift | 40 +++++++++++-------- iOS/SceneDelegate.swift | 15 ++++++- 12 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 Frameworks/Account/DeepLinkProvider.swift delete mode 100644 Shared/Activity/ActivityID.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 205b75f94..addab77c0 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -42,6 +42,7 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; }; 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; + 51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE1007234635A20056195D /* DeepLinkProvider.swift */; }; 552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; }; 552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; }; 552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; }; @@ -195,6 +196,7 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; + 51FE1007234635A20056195D /* DeepLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkProvider.swift; sourceTree = ""; }; 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = ""; }; 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = ""; }; 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = ""; }; @@ -431,6 +433,7 @@ 510BD112232C3E9D002692E4 /* FeedMetadataFile.swift */, 841974001F6DD1EC006346C4 /* Folder.swift */, 844B297E210CE37E004020B3 /* UnreadCountProvider.swift */, + 51FE1007234635A20056195D /* DeepLinkProvider.swift */, 5165D71F22835E9800D9D53D /* FeedFinder */, 515E4EB12324FF7D0057B0E7 /* Credentials */, 8419742B1F6DDE84006346C4 /* LocalAccount */, @@ -807,6 +810,7 @@ 51BB7B84233531BC008E8144 /* AccountBehaviors.swift in Sources */, 9E1773D923458D590056A5A8 /* FeedlyResourceId.swift in Sources */, 552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */, + 51FE1008234635A20056195D /* DeepLinkProvider.swift in Sources */, 9EC688EA232B973C00A8D0A2 /* FeedlyAPICaller.swift in Sources */, 9E1773D32345700F0056A5A8 /* FeedlyLink.swift in Sources */, 9EAEC62823331C350085D7C9 /* FeedlyCategory.swift in Sources */, diff --git a/Frameworks/Account/DeepLinkProvider.swift b/Frameworks/Account/DeepLinkProvider.swift new file mode 100644 index 000000000..5b97d2244 --- /dev/null +++ b/Frameworks/Account/DeepLinkProvider.swift @@ -0,0 +1,21 @@ +// +// DeepLinkProvider.swift +// Account +// +// Created by Maurice Parker on 10/3/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public enum DeepLinkKey: String { + case accountID = "accountID" + case accountName = "accountName" + case feedID = "feedID" + case articleID = "articleID" + case folderName = "folderName" +} + +public protocol DeepLinkProvider { + var deepLinkUserInfo: [AnyHashable : Any] { get } +} diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index c2855ef30..f9f6be0a3 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -11,7 +11,7 @@ import RSCore import RSWeb import Articles -public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Hashable { +public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, DeepLinkProvider, Hashable { public weak var account: Account? public let url: String @@ -179,6 +179,15 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, Ha account.renameFeed(self, to: newName, completion: completion) } + // MARK: - PathIDUserInfoProvider + public var deepLinkUserInfo: [AnyHashable : Any] { + return [ + DeepLinkKey.accountID.rawValue: account?.accountID ?? "", + DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.feedID.rawValue: feedID + ] + } + // MARK: - UnreadCountProvider public var unreadCount: Int { diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index 77261889a..cb5713ec3 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -10,7 +10,7 @@ import Foundation import Articles import RSCore -public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCountProvider, Hashable { +public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCountProvider, DeepLinkProvider, Hashable { public weak var account: Account? public var topLevelFeeds: Set = Set() @@ -32,6 +32,15 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun public var nameForDisplay: String { return name ?? Folder.untitledName } + + // MARK: - PathIDUserInfoProvider + public var deepLinkUserInfo: [AnyHashable : Any] { + return [ + DeepLinkKey.accountID.rawValue: account?.accountID ?? "", + DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.folderName.rawValue: nameForDisplay + ] + } // MARK: - UnreadCountProvider diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 21a1cdebc..8d9ac8097 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -94,7 +94,6 @@ 5183CCE9226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; 51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* ThemedNavigationController.swift */; }; 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; - 51934CD023108953006127BE /* ActivityID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCF23108953006127BE /* ActivityID.swift */; }; 51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; @@ -836,7 +835,6 @@ 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRefreshTimer.swift; sourceTree = ""; }; 51934CC1230F5963006127BE /* ThemedNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedNavigationController.swift; sourceTree = ""; }; 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = ""; }; - 51934CCF23108953006127BE /* ActivityID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityID.swift; sourceTree = ""; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = ""; }; 5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsImportDocumentPickerView.swift; sourceTree = ""; }; 5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsExportDocumentPickerView.swift; sourceTree = ""; }; @@ -1301,7 +1299,6 @@ isa = PBXGroup; children = ( 51934CCD2310792F006127BE /* ActivityManager.swift */, - 51934CCF23108953006127BE /* ActivityID.swift */, 51D87EE02311D34700E63F03 /* ActivityType.swift */, ); path = Activity; @@ -2895,7 +2892,6 @@ 51CC9B3E231720B2000E842F /* MasterFeedDataSource.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, - 51934CD023108953006127BE /* ActivityID.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, 513228FC233037630033D4ED /* Reachability.swift in Sources */, diff --git a/Shared/Activity/ActivityID.swift b/Shared/Activity/ActivityID.swift deleted file mode 100644 index 88b75e4a7..000000000 --- a/Shared/Activity/ActivityID.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ActivityID.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 8/23/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import Foundation - -enum ActivityID: String { - case accountID = "accountID" - case accountName = "accountName" - case feedID = "feedID" - case articleID = "articleID" - case folderName = "folderName" -} diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index efb30230c..a7772ca79 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -67,12 +67,7 @@ class ActivityManager { let title = NSString.localizedStringWithFormat(localizedText as NSString, folder.nameForDisplay) as String selectingActivity = makeSelectingActivity(type: ActivityType.selectFolder, title: title, identifier: ActivityManager.identifer(for: folder)) - selectingActivity!.userInfo = [ - ActivityID.accountID.rawValue: folder.account?.accountID ?? "", - ActivityID.accountName.rawValue: folder.account?.name ?? "", - ActivityID.folderName.rawValue: folder.nameForDisplay - ] - + selectingActivity!.userInfo = folder.deepLinkUserInfo selectingActivity!.becomeCurrent() } @@ -83,13 +78,8 @@ class ActivityManager { let title = NSString.localizedStringWithFormat(localizedText as NSString, feed.nameForDisplay) as String selectingActivity = makeSelectingActivity(type: ActivityType.selectFeed, title: title, identifier: ActivityManager.identifer(for: feed)) - selectingActivity!.userInfo = [ - ActivityID.accountID.rawValue: feed.account?.accountID ?? "", - ActivityID.accountName.rawValue: feed.account?.name ?? "", - ActivityID.feedID.rawValue: feed.feedID - ] + selectingActivity!.userInfo = feed.deepLinkUserInfo updateSelectingActivityFeedSearchAttributes(with: feed) - selectingActivity!.becomeCurrent() } @@ -155,7 +145,7 @@ class ActivityManager { } @objc func feedIconDidBecomeAvailable(_ note: Notification) { - guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ActivityID.feedID.rawValue] as? String else { + guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[DeepLinkKey.feedID.rawValue] as? String else { return } if activityFeedId == feed.feedID { @@ -190,12 +180,7 @@ private extension ActivityManager { let keywords = feedNameKeywords + articleTitleKeywords activity.keywords = Set(keywords) - activity.userInfo = [ - ActivityID.accountID.rawValue: article.accountID, - ActivityID.accountName.rawValue: article.account?.name ?? "", - ActivityID.feedID.rawValue: article.feedID, - ActivityID.articleID.rawValue: article.articleID - ] + activity.userInfo = article.deepLinkUserInfo activity.isEligibleForSearch = true activity.isEligibleForPrediction = false activity.isEligibleForHandoff = true diff --git a/Shared/Data/ArticleUtilities.swift b/Shared/Data/ArticleUtilities.swift index a2c6d733c..e33a56e33 100644 --- a/Shared/Data/ArticleUtilities.swift +++ b/Shared/Data/ArticleUtilities.swift @@ -91,6 +91,21 @@ extension Article { } +// MARK: PathIDUserInfoProvider + +extension Article: DeepLinkProvider { + + public var deepLinkUserInfo: [AnyHashable : Any] { + return [ + DeepLinkKey.accountID.rawValue: accountID, + DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.feedID.rawValue: feedID, + DeepLinkKey.articleID.rawValue: articleID + ] + } + +} + // MARK: SortableArticle extension Article: SortableArticle { diff --git a/Shared/UserNotifications/UserNotificationManager.swift b/Shared/UserNotifications/UserNotificationManager.swift index 842b39778..5ecf2f9b7 100644 --- a/Shared/UserNotifications/UserNotificationManager.swift +++ b/Shared/UserNotifications/UserNotificationManager.swift @@ -36,11 +36,16 @@ private extension UserNotificationManager { private func sendNotification(feed: Feed, article: Article) { let content = UNMutableNotificationContent() - + content.title = feed.nameForDisplay - content.body = article.title ?? article.summary ?? "" + content.body = TimelineStringFormatter.truncatedTitle(article) + if content.body.isEmpty { + content.body = TimelineStringFormatter.truncatedSummary(article) + } + content.sound = UNNotificationSound.default - + content.userInfo = article.deepLinkUserInfo + let request = UNNotificationRequest.init(identifier: "articleID:\(article.articleID)", content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index ab37ce762..39b5abe14 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -178,6 +178,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD completionHandler([.alert, .badge, .sound]) } + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + defer { completionHandler() } + + if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate { + sceneDelegate.handle(response) + } + + } + } // MARK: App Initialization diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a7803867c..5f3e5e5d8 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -7,6 +7,7 @@ // import UIKit +import UserNotifications import SwiftUI import Account import Articles @@ -315,16 +316,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { case .selectStarred: handleSelectStarred() case .selectFolder: - handleSelectFolder(activity) + handleSelectFolder(activity.userInfo) case .selectFeed: - handleSelectFeed(activity) + handleSelectFeed(activity.userInfo) case .nextUnread: selectFirstUnreadInAllUnread() case .readArticle: - handleReadArticle(activity) + handleReadArticle(activity.userInfo) } } + func handle(_ response: UNNotificationResponse) { + let userInfo = response.notification.request.content.userInfo + handleReadArticle(userInfo) + } + func configureThreePanelMode(for size: CGSize) { guard rootSplitViewController.traitCollection.userInterfaceIdiom == .pad && !rootSplitViewController.isCollapsed else { return @@ -1606,8 +1612,8 @@ private extension SceneCoordinator { } } - func handleSelectFolder(_ activity: NSUserActivity) { - guard let accountNode = findAccountNode(for: activity), let folderNode = findFolderNode(for: activity, beginningAt: accountNode) else { + func handleSelectFolder(_ userInfo: [AnyHashable : Any]?) { + guard let accountNode = findAccountNode(userInfo), let folderNode = findFolderNode(userInfo, beginningAt: accountNode) else { return } if let indexPath = indexPathFor(folderNode) { @@ -1615,8 +1621,8 @@ private extension SceneCoordinator { } } - func handleSelectFeed(_ activity: NSUserActivity) { - guard let accountNode = findAccountNode(for: activity), let feedNode = findFeedNode(for: activity, beginningAt: accountNode) else { + func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) { + guard let accountNode = findAccountNode(userInfo), let feedNode = findFeedNode(userInfo, beginningAt: accountNode) else { return } if let feed = feedNode.representedObject as? Feed { @@ -1624,14 +1630,14 @@ private extension SceneCoordinator { } } - func handleReadArticle(_ activity: NSUserActivity) { - guard let accountNode = findAccountNode(for: activity), let feedNode = findFeedNode(for: activity, beginningAt: accountNode) else { + func handleReadArticle(_ userInfo: [AnyHashable : Any]?) { + guard let accountNode = findAccountNode(userInfo), let feedNode = findFeedNode(userInfo, beginningAt: accountNode) else { return } discloseFeed(feedNode.representedObject as! Feed) { - guard let articleID = activity.userInfo?[ActivityID.articleID.rawValue] as? String else { return } + guard let articleID = userInfo?[DeepLinkKey.articleID.rawValue] as? String else { return } if let article = self.articles.first(where: { $0.articleID == articleID }) { self.selectArticle(article) } @@ -1639,8 +1645,8 @@ private extension SceneCoordinator { } } - func findAccountNode(for activity: NSUserActivity) -> Node? { - guard let accountID = activity.userInfo?[ActivityID.accountID.rawValue] as? String else { + func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? { + guard let accountID = userInfo?[DeepLinkKey.accountID.rawValue] as? String else { return nil } @@ -1648,7 +1654,7 @@ private extension SceneCoordinator { return node } - guard let accountName = activity.userInfo?[ActivityID.accountName.rawValue] as? String else { + guard let accountName = userInfo?[DeepLinkKey.accountName.rawValue] as? String else { return nil } @@ -1659,8 +1665,8 @@ private extension SceneCoordinator { return nil } - func findFolderNode(for activity: NSUserActivity, beginningAt startingNode: Node) -> Node? { - guard let folderName = activity.userInfo?[ActivityID.folderName.rawValue] as? String else { + func findFolderNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? { + guard let folderName = userInfo?[DeepLinkKey.folderName.rawValue] as? String else { return nil } if let node = startingNode.descendantNode(where: { ($0.representedObject as? Folder)?.nameForDisplay == folderName }) { @@ -1669,8 +1675,8 @@ private extension SceneCoordinator { return nil } - func findFeedNode(for activity: NSUserActivity, beginningAt startingNode: Node) -> Node? { - guard let feedID = activity.userInfo?[ActivityID.feedID.rawValue] as? String else { + func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? { + guard let feedID = userInfo?[DeepLinkKey.feedID.rawValue] as? String else { return nil } if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.feedID == feedID }) { diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 73785f010..b34b95dca 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import UserNotifications import Account class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -28,8 +29,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } + if let notificationResponse = connectionOptions.notificationResponse { + window!.makeKeyAndVisible() + coordinator.handle(notificationResponse) + return + } + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { - self.coordinator.handle(userActivity) + coordinator.handle(userActivity) } window!.makeKeyAndVisible() @@ -55,6 +62,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { return coordinator.stateRestorationActivity } + + // API + + func handle(_ response: UNNotificationResponse) { + coordinator.handle(response) + } }