Add the ability to handoff from iOS to Mac

This commit is contained in:
Maurice Parker 2019-10-03 15:49:27 -05:00
parent d41da79c72
commit 0000e03083
10 changed files with 55 additions and 12 deletions

View File

@ -183,7 +183,7 @@ public final class Feed: DisplayNameProvider, Renamable, UnreadCountProvider, De
public var deepLinkUserInfo: [AnyHashable : Any] { public var deepLinkUserInfo: [AnyHashable : Any] {
return [ return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "", DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
DeepLinkKey.accountName.rawValue: account?.name ?? "", DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.feedID.rawValue: feedID DeepLinkKey.feedID.rawValue: feedID
] ]
} }

View File

@ -37,7 +37,7 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
public var deepLinkUserInfo: [AnyHashable : Any] { public var deepLinkUserInfo: [AnyHashable : Any] {
return [ return [
DeepLinkKey.accountID.rawValue: account?.accountID ?? "", DeepLinkKey.accountID.rawValue: account?.accountID ?? "",
DeepLinkKey.accountName.rawValue: account?.name ?? "", DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.folderName.rawValue: nameForDisplay DeepLinkKey.folderName.rawValue: nameForDisplay
] ]
} }

View File

@ -211,6 +211,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
#endif #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 { func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
// https://github.com/brentsimmons/NetNewsWire/issues/522 // https://github.com/brentsimmons/NetNewsWire/issues/522

View File

@ -18,6 +18,8 @@ enum TimelineSourceMode {
class MainWindowController : NSWindowController, NSUserInterfaceValidations { class MainWindowController : NSWindowController, NSUserInterfaceValidations {
private var activityManager = ActivityManager()
private var isShowingExtractedArticle = false private var isShowingExtractedArticle = false
private var articleExtractor: ArticleExtractor? = nil private var articleExtractor: ArticleExtractor? = nil
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate? private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
@ -119,6 +121,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
currentTimelineViewController?.goToDeepLink(for: userInfo) 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 // MARK: - Notifications
// func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { // func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
@ -456,6 +464,8 @@ extension MainWindowController: SidebarDelegate {
extension MainWindowController: TimelineContainerViewControllerDelegate { extension MainWindowController: TimelineContainerViewControllerDelegate {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) { func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
activityManager.invalidateReading()
articleExtractor?.cancel() articleExtractor?.cancel()
articleExtractor = nil articleExtractor = nil
isShowingExtractedArticle = false isShowingExtractedArticle = false
@ -464,6 +474,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
let detailState: DetailState let detailState: DetailState
if let articles = articles { if let articles = articles {
if articles.count == 1 { if articles.count == 1 {
activityManager.reading(articles.first!)
if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false { if articles.first?.feed?.isArticleExtractorAlwaysOn ?? false {
detailState = .loading detailState = .loading
startArticleExtractorForCurrentLink() startArticleExtractorForCurrentLink()

View File

@ -488,7 +488,7 @@ private extension SidebarViewController {
return nil 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 return node
} }

View File

@ -43,6 +43,10 @@
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSUserActivityTypes</key>
<array>
<string>com.ranchero.NetNewsWire.ReadArticle</string>
</array>
<key>NSAppleEventsUsageDescription</key> <key>NSAppleEventsUsageDescription</key>
<string>NetNewsWire communicates with other apps on your Mac when you choose to share an article.</string> <string>NetNewsWire communicates with other apps on your Mac when you choose to share an article.</string>
<key>NSAppleScriptEnabled</key> <key>NSAppleScriptEnabled</key>

View File

@ -219,6 +219,8 @@
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; }; 51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FD413A2342BD0500880194 /* MasterTimelineUnreadCountView.swift */; };
51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; 51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; };
51FE10042345529D0056195D /* 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 */; }; 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; };
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; };
5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; }; 5F323809231DF9F000706F6B /* NNWTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* NNWTableViewCell.swift */; };
@ -2947,6 +2949,7 @@
51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */,
849A97651ED9EB96007D329B /* FeedTreeControllerDelegate.swift in Sources */, 849A97651ED9EB96007D329B /* FeedTreeControllerDelegate.swift in Sources */,
849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */, 849A97671ED9EB96007D329B /* UnreadCountView.swift in Sources */,
51FE10092346739D0056195D /* ActivityType.swift in Sources */,
840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */, 840BEE4121D70E64009BBAFA /* CrashReportWindowController.swift in Sources */,
8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */, 8426118A1FCB67AA0086A189 /* FeedIconDownloader.swift in Sources */,
84C9FC7B22629E1200D921D6 /* AccountsControlsBackgroundView.swift in Sources */, 84C9FC7B22629E1200D921D6 /* AccountsControlsBackgroundView.swift in Sources */,
@ -2973,6 +2976,7 @@
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */, 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */, 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */,
51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */, 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */,
51FE100A234673A00056195D /* ActivityManager.swift in Sources */,
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */, 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */,
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */, 5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,

View File

@ -113,6 +113,7 @@ class ActivityManager {
readingActivity = nil readingActivity = nil
} }
#if os(iOS)
static func cleanUp(_ account: Account) { static func cleanUp(_ account: Account) {
var ids = [String]() var ids = [String]()
@ -143,7 +144,8 @@ class ActivityManager {
static func cleanUp(_ feed: Feed) { static func cleanUp(_ feed: Feed) {
NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {} NSUserActivity.deleteSavedUserActivities(withPersistentIdentifiers: identifers(for: feed)) {}
} }
#endif
@objc func feedIconDidBecomeAvailable(_ note: Notification) { @objc func feedIconDidBecomeAvailable(_ note: Notification) {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[DeepLinkKey.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 return
@ -162,28 +164,33 @@ private extension ActivityManager {
func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity { func makeSelectingActivity(type: ActivityType, title: String, identifier: String) -> NSUserActivity {
let activity = NSUserActivity(activityType: type.rawValue) let activity = NSUserActivity(activityType: type.rawValue)
activity.title = title activity.title = title
activity.suggestedInvocationPhrase = title
activity.keywords = Set(makeKeywords(title)) activity.keywords = Set(makeKeywords(title))
activity.isEligibleForPrediction = true
activity.isEligibleForSearch = true activity.isEligibleForSearch = true
#if os(iOS)
activity.suggestedInvocationPhrase = title
activity.isEligibleForPrediction = true
activity.persistentIdentifier = identifier activity.persistentIdentifier = identifier
#endif
return activity return activity
} }
func makeReadArticleActivity(_ article: Article) -> NSUserActivity { func makeReadArticleActivity(_ article: Article) -> NSUserActivity {
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue) let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
activity.title = article.title activity.title = article.title
activity.userInfo = article.deepLinkUserInfo
activity.isEligibleForHandoff = true
#if os(iOS)
let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay) let feedNameKeywords = makeKeywords(article.feed?.nameForDisplay)
let articleTitleKeywords = makeKeywords(article.title) let articleTitleKeywords = makeKeywords(article.title)
let keywords = feedNameKeywords + articleTitleKeywords let keywords = feedNameKeywords + articleTitleKeywords
activity.keywords = Set(keywords) activity.keywords = Set(keywords)
activity.userInfo = article.deepLinkUserInfo
activity.isEligibleForSearch = true activity.isEligibleForSearch = true
activity.isEligibleForPrediction = false activity.isEligibleForPrediction = false
activity.isEligibleForHandoff = true
activity.persistentIdentifier = ActivityManager.identifer(for: article) activity.persistentIdentifier = ActivityManager.identifer(for: article)
// CoreSpotlight // CoreSpotlight
@ -197,7 +204,8 @@ private extension ActivityManager {
} }
activity.contentAttributeSet = attributeSet activity.contentAttributeSet = attributeSet
#endif
return activity return activity
} }
@ -211,9 +219,17 @@ private extension ActivityManager {
attributeSet.title = feed.nameForDisplay attributeSet.title = feed.nameForDisplay
attributeSet.keywords = makeKeywords(feed.nameForDisplay) attributeSet.keywords = makeKeywords(feed.nameForDisplay)
if let image = appDelegate.feedIconDownloader.icon(for: feed) { if let image = appDelegate.feedIconDownloader.icon(for: feed) {
#if os(iOS)
attributeSet.thumbnailData = image.pngData() attributeSet.thumbnailData = image.pngData()
#else
attributeSet.thumbnailData = image.tiffRepresentation
#endif
} else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) { } else if let image = appDelegate.faviconDownloader.faviconAsAvatar(for: feed) {
#if os(iOS)
attributeSet.thumbnailData = image.pngData() attributeSet.thumbnailData = image.pngData()
#else
attributeSet.thumbnailData = image.tiffRepresentation
#endif
} }
selectingActivity!.contentAttributeSet = attributeSet selectingActivity!.contentAttributeSet = attributeSet

View File

@ -98,7 +98,7 @@ extension Article: DeepLinkProvider {
public var deepLinkUserInfo: [AnyHashable : Any] { public var deepLinkUserInfo: [AnyHashable : Any] {
return [ return [
DeepLinkKey.accountID.rawValue: accountID, DeepLinkKey.accountID.rawValue: accountID,
DeepLinkKey.accountName.rawValue: account?.name ?? "", DeepLinkKey.accountName.rawValue: account?.nameForDisplay ?? "",
DeepLinkKey.feedID.rawValue: feedID, DeepLinkKey.feedID.rawValue: feedID,
DeepLinkKey.articleID.rawValue: articleID DeepLinkKey.articleID.rawValue: articleID
] ]

View File

@ -1658,7 +1658,7 @@ private extension SceneCoordinator {
return nil 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 return node
} }