From a89fb56a60e79291383e40df16ad3d84e9b169a7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 4 Jun 2019 17:04:37 -0400 Subject: [PATCH] Add support for iOS 13 previewing and actions --- .../ConversationTableViewController.swift | 2 - .../NotificationsTableViewController.swift | 2 - .../Profile/ProfileTableViewController.swift | 2 - .../TimelineTableViewController.swift | 4 +- .../EnhancedTableViewController.swift | 33 +++++++++ .../Utilities/LoadingViewController.swift | 2 +- Tusker/Screens/Utilities/Previewing.swift | 70 ++++++++++++++----- .../ActionNotificationTableViewCell.swift | 25 +++++-- .../FollowNotificationTableViewCell.swift | 6 +- .../ProfileHeaderTableViewCell.swift | 24 +++++-- .../ConversationMainStatusTableViewCell.swift | 26 +++++-- Tusker/Views/Status/StatusTableViewCell.swift | 28 ++++++-- 12 files changed, 168 insertions(+), 56 deletions(-) diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index d50a4b46..4f014238 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -58,8 +58,6 @@ class ConversationTableViewController: EnhancedTableViewController { self.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) } } - - registerForPreviewing(with: self, sourceView: view) } override func viewWillAppear(_ animated: Bool) { diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 6deb2a0f..c19adb7e 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -56,8 +56,6 @@ class NotificationsTableViewController: EnhancedTableViewController { self.older = pagination?.older } - registerForPreviewing(with: self, sourceView: view) - userActivity = UserActivityManager.checkNotificationsActivity() } diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 5398261d..862c3808 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -89,8 +89,6 @@ class ProfileTableViewController: EnhancedTableViewController, PreferencesAdapti add(loadingVC!) shouldLoadOnAccountIDSet = true } - - registerForPreviewing(with: self, sourceView: view) } override func viewWillAppear(_ animated: Bool) { diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index f688aad2..587de5e9 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -82,8 +82,6 @@ class TimelineTableViewController: EnhancedTableViewController { self.newer = pagination?.newer self.older = pagination?.older } - - registerForPreviewing(with: self, sourceView: view) } override func viewWillAppear(_ animated: Bool) { @@ -119,6 +117,8 @@ class TimelineTableViewController: EnhancedTableViewController { return cell } + // MARK: - Table view delegate + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if indexPath.row == statusIDs.count - 1 { guard let older = older else { return } diff --git a/Tusker/Screens/Utilities/EnhancedTableViewController.swift b/Tusker/Screens/Utilities/EnhancedTableViewController.swift index 92f9d0a1..b97f4c93 100644 --- a/Tusker/Screens/Utilities/EnhancedTableViewController.swift +++ b/Tusker/Screens/Utilities/EnhancedTableViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SafariServices class EnhancedTableViewController: UITableViewController { @@ -36,3 +37,35 @@ class EnhancedTableViewController: UITableViewController { } } + +extension EnhancedTableViewController { + + override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + if let cell = tableView.cellForRow(at: indexPath) as? UITableViewCell & MenuPreviewProvider { + let cellLocation = cell.convert(point, from: tableView) + guard let (previewProvider, actionsProvider) = cell.getPreviewProviders(for: cellLocation, sourceViewController: self) else { + return nil + } + let actionProvider: UIContextMenuActionProvider = { (elements) in + return UIMenu.create(title: "test", children: elements + actionsProvider()) + } + return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: actionProvider) + } else { + return nil + } + } + + override func tableView(_ tableView: UITableView, willCommitMenuWithAnimator animator: UIContextMenuInteractionCommitAnimating) { + if /*animator.preferredCommitStyle == .pop,*/ // preferredCommitStyle is always .dismiss, see FB6113554 + let viewController = animator.previewViewController { + animator.addCompletion { + if viewController is LargeImageViewController || viewController is SFSafariViewController { + self.present(viewController, animated: true) + } else { + self.show(viewController, sender: nil) + } + } + } + } + +} diff --git a/Tusker/Screens/Utilities/LoadingViewController.swift b/Tusker/Screens/Utilities/LoadingViewController.swift index e2196e75..d5a79211 100644 --- a/Tusker/Screens/Utilities/LoadingViewController.swift +++ b/Tusker/Screens/Utilities/LoadingViewController.swift @@ -16,7 +16,7 @@ class LoadingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - activityIndicator = UIActivityIndicatorView(style: .whiteLarge) + activityIndicator = UIActivityIndicatorView(style: .large) activityIndicator.color = .darkGray activityIndicator.translatesAutoresizingMaskIntoConstraints = false diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 27d36113..b68c3671 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -8,31 +8,65 @@ import UIKit import SafariServices +import Pachyderm -protocol PreviewViewControllerProvider { +protocol MenuPreviewProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? + typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIAction]) + + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? } -@objc extension UITableViewController: UIViewControllerPreviewingDelegate { - public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { - if let indexPath = tableView.indexPathForRow(at: location), - let cell = tableView.cellForRow(at: indexPath) as? UITableViewCell & PreviewViewControllerProvider { - let cellLocation = cell.convert(location, from: tableView) - if let vc = cell.getPreviewViewController(forLocation: cellLocation, sourceViewController: self) { -// previewingContext.sourceRect = tableView.rectForRow(at: indexPath) - return vc +extension MenuPreviewProvider { + + fileprivate func present(_ vc: UIViewController) { + UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true) + } + + func actionsForProfile(accountID: String) -> [UIAction] { + guard let account = MastodonCache.account(for: accountID) else { return [] } + return [ + UIAction(__title: "Open in Safari", image: UIImage(systemName: "safari")) { (_) in + self.present(SFSafariViewController(url: account.url)) + }, + UIAction(__title: "Send Message", image: UIImage(systemName: "envelope")) { (_) in + self.present(UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct))) + }, + UIAction(__title: "Share...", image: UIImage(systemName: "square.and.arrow.up")) { (_) in + self.present(UIActivityViewController(activityItems: [account.url], applicationActivities: nil)) } - } - return nil + ] } - public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { - if viewControllerToCommit is LargeImageViewController || viewControllerToCommit is SFSafariViewController { - present(viewControllerToCommit, animated: false) - } else { - navigationController!.pushViewController(viewControllerToCommit, animated: false) - } + func actionsForURL(_ url: URL) -> [UIAction] { + return [ + UIAction(__title: "Open in Safari", image: UIImage(systemName: "safari")) { (_) in + self.present(SFSafariViewController(url: url)) + }, + UIAction(__title: "Share...", image: UIImage(systemName: "square.and.arrow.up")) { (_) in + self.present(UIActivityViewController(activityItems: [url], applicationActivities: nil)) + } + ] } + + func actionsForHashtag(_ hashtag: Hashtag) -> [UIAction] { + return actionsForURL(hashtag.url) + } + + func actionsForStatus(statusID: String) -> [UIAction] { + guard let status = MastodonCache.status(for: statusID) else { return [] } + return [ + UIAction(__title: "Reply", image: UIImage(systemName: "arrowshape.turn.up.left")) { (_) in + self.present(UINavigationController(rootViewController: ComposeViewController(inReplyTo: statusID))) + }, + UIAction(__title: "Open in Safari", image: UIImage(systemName: "safari")) { (_) in + self.present(SFSafariViewController(url: status.url!)) + }, + UIAction(__title: "Share...", image: UIImage(systemName: "square.and.arrow.up")) { (_) in + self.present(UIActivityViewController(activityItems: [status.url!], applicationActivities: nil)) + } + ] + } + } diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift index d2d50f21..4d6e9313 100644 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift @@ -179,14 +179,27 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { } -extension ActionNotificationTableViewCell: PreviewViewControllerProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { +extension ActionNotificationTableViewCell: MenuPreviewProvider { + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { if avatarContainerView.frame.contains(location) { - return ProfileTableViewController(accountID: notification.account.id) + let accountID = notification.account.id + return (content: { ProfileTableViewController(accountID: accountID) }, actions: { self.actionsForProfile(accountID: accountID) }) } else if contentLabel.frame.contains(location), - let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { - return vc + let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) { + return ( + content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) }, + actions: { + let text = (self.contentLabel.text! as NSString).substring(with: link.range) + if let mention = self.contentLabel.getMention(for: link.url, text: text) { + return self.actionsForProfile(accountID: mention.id) + } else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) { + return self.actionsForHashtag(hashtag) + } else { + return self.actionsForURL(link.url) + } + } + ) } - return ConversationTableViewController(for: statusID) + return (content: { ConversationTableViewController(for: self.statusID) }, actions: { self.actionsForStatus(statusID: self.statusID) }) } } diff --git a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift index f5899f95..d14c7e50 100644 --- a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift @@ -98,8 +98,8 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { } -extension FollowNotificationTableViewCell: PreviewViewControllerProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { - return ProfileTableViewController(accountID: accountID) +extension FollowNotificationTableViewCell: MenuPreviewProvider { + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { + return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) } } diff --git a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift index 8b748767..8087226f 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderTableViewCell.swift @@ -141,14 +141,26 @@ class ProfileHeaderTableViewCell: UITableViewCell, PreferencesAdaptive { } -extension ProfileHeaderTableViewCell: PreviewViewControllerProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { +extension ProfileHeaderTableViewCell: MenuPreviewProvider { + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { let noteLabelPoint = noteLabel.convert(location, from: self) if noteLabel.bounds.contains(noteLabelPoint), - let vc = noteLabel.getViewController(forLinkAt: noteLabelPoint) { - return vc + let link = noteLabel.getLink(atPoint: noteLabelPoint) { + return ( + content: { self.noteLabel.getViewController(forLink: link.url, inRange: link.range) }, + actions: { + let text = (self.noteLabel.text! as NSString).substring(with: link.range) + if let mention = self.noteLabel.getMention(for: link.url, text: text) { + return self.actionsForProfile(accountID: mention.id) + } else if let hashtag = self.noteLabel.getHashtag(for: link.url, text: text) { + return self.actionsForHashtag(hashtag) + } else { + return self.actionsForURL(link.url) + } + } + ) + } else { + return nil } - // TODO: should this also have peek/pop for avatar/header images? - return nil } } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index dee029be..6d96dded 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -234,20 +234,32 @@ extension ConversationMainStatusTableViewCell: AttachmentViewDelegate { } } -extension ConversationMainStatusTableViewCell: PreviewViewControllerProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { +extension ConversationMainStatusTableViewCell: MenuPreviewProvider { + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { if avatarImageView.frame.contains(location) { - return ProfileTableViewController(accountID: accountID) + return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) } else if attachmentsView.frame.contains(location) { let attachmentsViewLocation = attachmentsView.convert(location, from: self) if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView { let image = attachmentView.image! - let description = attachmentView.description - return delegate?.largeImage(image, description: description, sourceView: attachmentView) + let description = attachmentView.attachment.description + return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] }) } } else if contentLabel.frame.contains(location), - let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { - return vc + let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) { + return ( + content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) }, + actions: { + let text = (self.contentLabel.text! as NSString).substring(with: link.range) + if let mention = self.contentLabel.getMention(for: link.url, text: text) { + return self.actionsForProfile(accountID: mention.id) + } else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) { + return self.actionsForHashtag(hashtag) + } else { + return self.actionsForURL(link.url) + } + } + ) } return nil } diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index ce809c64..abd7048a 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -361,21 +361,35 @@ extension StatusTableViewCell: AttachmentViewDelegate { } } -extension StatusTableViewCell: PreviewViewControllerProvider { - func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { +extension StatusTableViewCell: MenuPreviewProvider { + + func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? { if avatarImageView.frame.contains(location) { - return ProfileTableViewController(accountID: accountID) + return (content: { ProfileTableViewController(accountID: self.accountID) }, actions: { self.actionsForProfile(accountID: self.accountID) }) } else if attachmentsView.frame.contains(location) { let attachmentsViewLocation = attachmentsView.convert(location, from: self) if let attachmentView = attachmentsView.subviews.first(where: { $0.frame.contains(attachmentsViewLocation) }) as? AttachmentView { let image = attachmentView.image! let description = attachmentView.attachment.description - return delegate?.largeImage(image, description: description, sourceView: attachmentView) + return (content: { self.delegate?.largeImage(image, description: description, sourceView: attachmentView) }, actions: { [] }) } } else if contentLabel.frame.contains(location), - let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { - return vc + let link = contentLabel.getLink(atPoint: contentLabel.convert(location, from: self)) { + return ( + content: { self.contentLabel.getViewController(forLink: link.url, inRange: link.range) }, + actions: { + let text = (self.contentLabel.text! as NSString).substring(with: link.range) + if let mention = self.contentLabel.getMention(for: link.url, text: text) { + return self.actionsForProfile(accountID: mention.id) + } else if let hashtag = self.contentLabel.getHashtag(for: link.url, text: text) { + return self.actionsForHashtag(hashtag) + } else { + return self.actionsForURL(link.url) + } + } + ) } - return ConversationTableViewController(for: statusID) + return (content: { ConversationTableViewController(for: self.statusID) }, actions: { self.actionsForStatus(statusID: self.statusID) }) } + }