Add notification deep linking for iOS

This commit is contained in:
Maurice Parker 2019-10-03 09:53:21 -05:00
parent 046ec7de51
commit 0c9a1ba8d0
12 changed files with 118 additions and 63 deletions

View File

@ -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 = "<group>"; };
51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = "<group>"; };
51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = "<group>"; };
51FE1007234635A20056195D /* DeepLinkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkProvider.swift; sourceTree = "<group>"; };
552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = "<group>"; };
552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = "<group>"; };
552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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 }
}

View File

@ -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 {

View File

@ -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<Feed> = Set<Feed>()
@ -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

View File

@ -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 = "<group>"; };
51934CC1230F5963006127BE /* ThemedNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedNavigationController.swift; sourceTree = "<group>"; };
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
51934CCF23108953006127BE /* ActivityID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityID.swift; sourceTree = "<group>"; };
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsImportDocumentPickerView.swift; sourceTree = "<group>"; };
5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsExportDocumentPickerView.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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"
}

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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 }) {

View File

@ -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)
}
}