From 0000e030832b1a2aec3dd5e1875307caaf3d672c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 3 Oct 2019 15:49:27 -0500 Subject: [PATCH] Add the ability to handoff from iOS to Mac --- Frameworks/Account/Feed.swift | 2 +- Frameworks/Account/Folder.swift | 2 +- Mac/AppDelegate.swift | 8 +++++ Mac/MainWindow/MainWindowController.swift | 11 +++++++ .../Sidebar/SidebarViewController.swift | 2 +- Mac/Resources/Info.plist | 4 +++ NetNewsWire.xcodeproj/project.pbxproj | 4 +++ Shared/Activity/ActivityManager.swift | 30 ++++++++++++++----- Shared/Data/ArticleUtilities.swift | 2 +- iOS/SceneCoordinator.swift | 2 +- 10 files changed, 55 insertions(+), 12 deletions(-) diff --git a/Frameworks/Account/Feed.swift b/Frameworks/Account/Feed.swift index f9f6be0a3..0ca76adcd 100644 --- a/Frameworks/Account/Feed.swift +++ b/Frameworks/Account/Feed.swift @@ -183,7 +183,7 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De public var deepLinkUserInfo: [AnyHashable : Any] { return [ DeepLinkKey.accountID.rawValue: account?.accountID ?? "", - DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "", DeepLinkKey.feedID.rawValue: feedID ] } diff --git a/Frameworks/Account/Folder.swift b/Frameworks/Account/Folder.swift index cb5713ec3..c341e110a 100644 --- a/Frameworks/Account/Folder.swift +++ b/Frameworks/Account/Folder.swift @@ -37,7 +37,7 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun public var deepLinkUserInfo: [AnyHashable : Any] { return [ DeepLinkKey.accountID.rawValue: account?.accountID ?? "", - DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "", DeepLinkKey.folderName.rawValue: nameForDisplay ] } diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index be543d33f..ec7cc7595 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -211,6 +211,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, } #endif } + + func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { + guard let mainWindowController = mainWindowController else { + return false + } + mainWindowController.handle(userActivity) + return true + } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { // https://github.com/brentsimmons/NetNewsWire/issues/522 diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index 7f3861ce2..d4212211b 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -18,6 +18,8 @@ enum TimelineSourceMode { class MainWindowController : NSWindowController, NSUserInterfaceValidations { + private var activityManager = ActivityManager() + private var isShowingExtractedArticle = false private var articleExtractor: ArticleExtractor? = nil private var sharingServicePickerDelegate: NSSharingServicePickerDelegate? @@ -119,6 +121,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { currentTimelineViewController?.goToDeepLink(for: userInfo) } + func handle(_ activity: NSUserActivity) { + guard let userInfo = activity.userInfo else { return } + sidebarViewController?.deepLinkRevealAndSelect(for: userInfo) + currentTimelineViewController?.goToDeepLink(for: userInfo) + } + // MARK: - Notifications // func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { @@ -456,6 +464,8 @@ extension MainWindowController: SidebarDelegate { extension MainWindowController: TimelineContainerViewControllerDelegate { func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) { + activityManager.invalidateReading() + articleExtractor?.cancel() articleExtractor = nil isShowingExtractedArticle = false @@ -464,6 +474,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { let detailState: DetailState if let articles = articles { if articles.count == 1 { + activityManager.reading(articles.first!) if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false { detailState = .loading startArticleExtractorForCurrentLink() diff --git a/Mac/MainWindow/Sidebar/SidebarViewController.swift b/Mac/MainWindow/Sidebar/SidebarViewController.swift index ab588dc5e..9924d5de5 100644 --- a/Mac/MainWindow/Sidebar/SidebarViewController.swift +++ b/Mac/MainWindow/Sidebar/SidebarViewController.swift @@ -488,7 +488,7 @@ private extension SidebarViewController { return nil } - if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.name == accountName }) { + if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) { return node } diff --git a/Mac/Resources/Info.plist b/Mac/Resources/Info.plist index 31bd46543..a9c54ee1d 100644 --- a/Mac/Resources/Info.plist +++ b/Mac/Resources/Info.plist @@ -43,6 +43,10 @@ NSAllowsArbitraryLoads + NSUserActivityTypes + + com.ranchero.NetNewsWire.ReadArticle + NSAppleEventsUsageDescription NetNewsWire communicates with other apps on your Mac when you choose to share an article. NSAppleScriptEnabled diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 8d9ac8097..10fec3605 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -219,6 +219,8 @@ 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; }; 51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; 51FE10042345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; + 51FE10092346739D0056195D /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; + 51FE100A234673A00056195D /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; @@ -2947,6 +2949,7 @@ 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 849A97651ED9EB96007D329B /* FeedTreeControllerDelegate.swift in Sources */, 849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */, + 51FE10092346739D0056195D /* ActivityType.swift in Sources */, 840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */, 8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */, 84C9FC7B22629E1200D921D6 /* AccountsControlsBackgroundView.swift in Sources */, @@ -2973,6 +2976,7 @@ 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */, 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */, 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */, + 51FE100A234673A00056195D /* ActivityManager.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */, 5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */, diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index a7772ca79..c663fcbc5 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -113,6 +113,7 @@ class ActivityManager { readingActivity = nil } + #if os(iOS) static func cleanUp(_ account: Account) { var ids = [String]() @@ -143,7 +144,8 @@ class ActivityManager { static func cleanUp(_ feed: Feed) { NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {} } - + #endif + @objc func feedIconDidBecomeAvailable(_ note: Notification) { guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[DeepLinkKey.feedID.rawValue] as? String else { return @@ -162,28 +164,33 @@ private extension ActivityManager { func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity { let activity = NSUserActivity(activityType: type.rawValue) activity.title = title - activity.suggestedInvocationPhrase = title activity.keywords = Set(makeKeywords(title)) - activity.isEligibleForPrediction = true activity.isEligibleForSearch = true + + #if os(iOS) + activity.suggestedInvocationPhrase = title + activity.isEligibleForPrediction = true activity.persistentIdentifier = identifier + #endif + return activity } func makeReadArticleActivity(_ article: Article) -> NSUserActivity { let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) - activity.title = article.title + activity.userInfo = article.deepLinkUserInfo + activity.isEligibleForHandoff = true + + #if os(iOS) let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay) let articleTitleKeywords = makeKeywords(article.title) let keywords = feedNameKeywords + articleTitleKeywords activity.keywords = Set(keywords) - activity.userInfo = article.deepLinkUserInfo activity.isEligibleForSearch = true activity.isEligibleForPrediction = false - activity.isEligibleForHandoff = true activity.persistentIdentifier = ActivityManager.identifer(for: article) // CoreSpotlight @@ -197,7 +204,8 @@ private extension ActivityManager { } activity.contentAttributeSet = attributeSet - + #endif + return activity } @@ -211,9 +219,17 @@ private extension ActivityManager { attributeSet.title = feed.nameForDisplay attributeSet.keywords = makeKeywords(feed.nameForDisplay) if let image = appDelegate.feedIconDownloader.icon(for: feed) { + #if os(iOS) attributeSet.thumbnailData = image.pngData() + #else + attributeSet.thumbnailData = image.tiffRepresentation + #endif } else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { + #if os(iOS) attributeSet.thumbnailData = image.pngData() + #else + attributeSet.thumbnailData = image.tiffRepresentation + #endif } selectingActivity!.contentAttributeSet = attributeSet diff --git a/Shared/Data/ArticleUtilities.swift b/Shared/Data/ArticleUtilities.swift index e33a56e33..e3b2eb88e 100644 --- a/Shared/Data/ArticleUtilities.swift +++ b/Shared/Data/ArticleUtilities.swift @@ -98,7 +98,7 @@ extension Article: DeepLinkProvider { public var deepLinkUserInfo: [AnyHashable : Any] { return [ DeepLinkKey.accountID.rawValue: accountID, - DeepLinkKey.accountName.rawValue: account?.name ?? "", + DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "", DeepLinkKey.feedID.rawValue: feedID, DeepLinkKey.articleID.rawValue: articleID ] diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 5f3e5e5d8..e78178057 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1658,7 +1658,7 @@ private extension SceneCoordinator { return nil } - if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.name == accountName }) { + if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.nameForDisplay == accountName }) { return node }