From 7e8f22c4716aa45cc5ea92f0fa7d2db3e39d98c9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 20 Oct 2018 22:07:04 -0400 Subject: [PATCH] Refactor view controller creation/navigation into AppRouter --- Tusker.xcodeproj/project.pbxproj | 4 + Tusker/AppDelegate.swift | 5 +- Tusker/AppRouter.swift | 127 ++++++++++++++++++ .../UIViewController+Delegates.swift | 8 -- .../Compose/ComposeViewController.swift | 9 +- .../ConversationTableViewController.swift | 12 +- .../LargeImageViewController.swift | 28 ++-- .../Main/MainTabBarViewController.swift | 12 +- .../NotificationsTableViewController.swift | 8 +- .../Profile/ProfileTableViewController.swift | 16 ++- .../TimelineTableViewController.swift | 9 +- Tusker/Shortcuts/UserActivityManager.swift | 14 +- Tusker/TuskerNavigationDelegate.swift | 127 ++++-------------- Tusker/Views/ContentLabel.swift | 6 +- .../ActionNotificationTableViewCell.swift | 4 +- .../FollowNotificationTableViewCell.swift | 4 +- .../ConversationMainStatusTableViewCell.swift | 4 +- Tusker/Views/Status/StatusTableViewCell.swift | 6 +- Tusker/XCallbackURL/XCBActions.swift | 36 +++-- 19 files changed, 246 insertions(+), 193 deletions(-) create mode 100644 Tusker/AppRouter.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 155076b0..ab390cd5 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ D621544821682A9D0003D87D /* TabsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544721682A9D0003D87D /* TabsTableViewController.swift */; }; D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544A21682AD30003D87D /* TabTableViewCell.swift */; }; D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D621544C21682AD90003D87D /* TabTableViewCell.xib */; }; + D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627FF73217BBC9700CC0648 /* AppRouter.swift */; }; D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; }; D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; }; D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; }; @@ -247,6 +248,7 @@ D621544721682A9D0003D87D /* TabsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTableViewController.swift; sourceTree = ""; }; D621544A21682AD30003D87D /* TabTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTableViewCell.swift; sourceTree = ""; }; D621544C21682AD90003D87D /* TabTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TabTableViewCell.xib; sourceTree = ""; }; + D627FF73217BBC9700CC0648 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = ""; }; D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = ""; }; D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = ""; }; D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = ""; }; @@ -743,6 +745,7 @@ isa = PBXGroup; children = ( D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, + D627FF73217BBC9700CC0648 /* AppRouter.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, 04DACE8D212CC7CC009840C4 /* ImageCache.swift */, D6028B9A2150811100F223B9 /* MastodonCache.swift */, @@ -1137,6 +1140,7 @@ D6C693CA2161253F007D6A6D /* SilentActionPermissionsTableViewController.swift in Sources */, D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */, D641C777213CAA9E004B4513 /* ActionNotificationTableViewCell.swift in Sources */, + D627FF74217BBC9700CC0648 /* AppRouter.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D663626821360E2C00C9CBA2 /* PreferencesTableViewController.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 545f777c..7296cc27 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -12,6 +12,7 @@ import UIKit class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + var router: AppRouter! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { window = UIWindow(frame: UIScreen.main.bounds) @@ -66,7 +67,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { MastodonController.getOwnAccount() MastodonController.getOwnInstance() - window!.rootViewController = MainTabBarViewController() + let tabBarController = MainTabBarViewController() + window!.rootViewController = tabBarController + router = tabBarController.router } func showOnboardingUI() { diff --git a/Tusker/AppRouter.swift b/Tusker/AppRouter.swift new file mode 100644 index 00000000..4cd77cf0 --- /dev/null +++ b/Tusker/AppRouter.swift @@ -0,0 +1,127 @@ +// +// AppRouter.swift +// Tusker +// +// Created by Shadowfacts on 10/20/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm +import SafariServices + +class AppRouter { + + let rootViewController: UIViewController + var currentViewController: UIViewController { + return getContentViewController(from: rootViewController) + } + + init(root: UIViewController) { + self.rootViewController = root + } + + private func getContentViewController(from vc: UIViewController) -> UIViewController { + if let vc = vc as? UITabBarController, + let selected = vc.selectedViewController { + return getContentViewController(from: selected) + } else if let vc = vc as? UINavigationController, + let top = vc.topViewController { + return getContentViewController(from: top) + } else { + return vc + } + } + + func present(_ vc: UIViewController, animated: Bool) { + if currentViewController.presentingViewController != nil { + currentViewController.dismiss(animated: animated) { + self.present(vc, animated: animated) + } + } + currentViewController.present(vc, animated: animated) + } + + func push(_ vc: UIViewController, animated: Bool) { + if currentViewController.presentingViewController != nil { + currentViewController.dismiss(animated: animated) { + self.push(vc, animated: animated) + } + } + guard let nav = currentViewController.navigationController else { fatalError("Can't push view controller while not in navigation controller") } + nav.pushViewController(vc, animated: animated) + } + + func profile(for accountID: String) -> ProfileTableViewController { + return ProfileTableViewController(accountID: accountID, router: self) + } + + func profile(for mention: Mention) -> ProfileTableViewController { + return ProfileTableViewController(accountID: mention.id, router: self) + } + + func timeline(for timeline: Timeline) -> TimelineTableViewController { + return TimelineTableViewController(for: timeline, router: self) + } + + func timeline(tag: Hashtag) -> TimelineTableViewController { + return timeline(for: .tag(hashtag: tag.name)) + } + + func safariViewController(for url: URL) -> SFSafariViewController { + return SFSafariViewController(url: url) + } + + func conversation(for statusID: String) -> ConversationTableViewController { + return ConversationTableViewController(for: statusID, router: self) + } + + func compose(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> ComposeViewController { + return ComposeViewController(inReplyTo: inReplyToID, mentioningAcct: mentioningAcct, text: text, router: self) + } + + func largeImage(_ image: UIImage, description: String?, sourceFrame: CGRect, sourceCornerRadius: CGFloat, transitioningDelegate: UIViewControllerTransitioningDelegate?) -> LargeImageViewController { + let vc = LargeImageViewController(image: image, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius, router: self) + vc.transitioningDelegate = transitioningDelegate + return vc + } + + func moreOptions(forStatus statusID: String) -> UIAlertController { + guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } + + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + if let url = status.url { + alert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { (_) in + let vc = SFSafariViewController(url: url) + self.present(vc, animated: true) + })) + alert.addAction(UIAlertAction(title: "Copy", style: .default, handler: { (_) in + UIPasteboard.general.url = url + })) + alert.addAction(UIAlertAction(title: "Share...", style: .default, handler: { (_) in + let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil) + self.present(vc, animated: true) + })) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + return alert + } + + func moreOptions(forURL url: URL) -> UIAlertController { + let alert = UIAlertController(title: nil, message: url.absoluteString, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { (_) in + let vc = SFSafariViewController(url: url) + self.present(vc, animated: true) + })) + alert.addAction(UIAlertAction(title: "Copy", style: .default, handler: { (_) in + UIPasteboard.general.url = url + })) + alert.addAction(UIAlertAction(title: "Share...", style: .default, handler: { (_) in + let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil) + self.present(vc, animated: true) + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + return alert + } + +} diff --git a/Tusker/Extensions/UIViewController+Delegates.swift b/Tusker/Extensions/UIViewController+Delegates.swift index 5f2b3ec7..0c6d5fb6 100644 --- a/Tusker/Extensions/UIViewController+Delegates.swift +++ b/Tusker/Extensions/UIViewController+Delegates.swift @@ -7,14 +7,6 @@ // import UIKit -import Pachyderm -import SafariServices - -extension LargeImageViewControllerDelegate where Self: UIViewController { - func closeLargeImage() { - dismiss(animated: true) - } -} extension UIViewController: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { diff --git a/Tusker/Screens/Compose/ComposeViewController.swift b/Tusker/Screens/Compose/ComposeViewController.swift index 82202c56..1fa8b2e6 100644 --- a/Tusker/Screens/Compose/ComposeViewController.swift +++ b/Tusker/Screens/Compose/ComposeViewController.swift @@ -12,6 +12,8 @@ import Intents class ComposeViewController: UIViewController { + let router: AppRouter + @IBOutlet weak var scrollView: UIScrollView! @IBOutlet weak var inReplyToContainerView: UIView! @IBOutlet weak var inReplyToAvatarImageView: UIImageView! @@ -51,11 +53,14 @@ class ComposeViewController: UIViewController { var status: Status? - init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) { - super.init(nibName: "ComposeViewController", bundle: nil) + init(inReplyTo inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil, router: AppRouter) { self.inReplyToID = inReplyToID self.mentioningAcct = mentioningAcct self.text = text + self.router = router + + super.init(nibName: "ComposeViewController", bundle: nil) + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed)) } diff --git a/Tusker/Screens/Conversation/ConversationTableViewController.swift b/Tusker/Screens/Conversation/ConversationTableViewController.swift index 73f6dc8d..e0ab9279 100644 --- a/Tusker/Screens/Conversation/ConversationTableViewController.swift +++ b/Tusker/Screens/Conversation/ConversationTableViewController.swift @@ -11,8 +11,9 @@ import Pachyderm class ConversationTableViewController: UITableViewController { - var mainStatusID: String! + let router: AppRouter + var mainStatusID: String! var statusIDs: [String] = [] { didSet { DispatchQueue.main.async { @@ -21,9 +22,11 @@ class ConversationTableViewController: UITableViewController { } } - init(for mainStatusID: String) { - super.init(style: .plain) + init(for mainStatusID: String, router: AppRouter) { self.mainStatusID = mainStatusID + self.router = router + + super.init(style: .plain) } required init?(coder aDecoder: NSCoder) { @@ -74,7 +77,7 @@ class ConversationTableViewController: UITableViewController { if let status = MastodonCache.status(for: mainStatusID), let url = status.url { actions.append(UIPreviewAction(title: "Open in Safari", style: .default, handler: { (_, _) in - let vc = self.viewController(forURL: url) + let vc = self.router.safariViewController(for: url) UIApplication.shared.delegate!.window!!.rootViewController!.present(vc, animated: true) })) actions.append(UIPreviewAction(title: "Share", style: .default, handler: { (_, _) in @@ -145,4 +148,3 @@ class ConversationTableViewController: UITableViewController { } extension ConversationTableViewController: StatusTableViewCellDelegate {} -extension ConversationTableViewController: LargeImageViewControllerDelegate {} diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index 32d694dd..bbd6937d 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -9,13 +9,9 @@ import UIKit import Photos -protocol LargeImageViewControllerDelegate { - func closeLargeImage() -} - class LargeImageViewController: UIViewController, UIScrollViewDelegate { - var delegate: LargeImageViewControllerDelegate? + let router: AppRouter var originFrame: CGRect? var originCornerRadius: CGFloat? @@ -64,22 +60,14 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate { return true } - init(image: UIImage, description: String?, sourceView: UIView, sourceViewController: UIViewController) { - super.init(nibName: "LargeImageViewController", bundle: nil) + init(image: UIImage, description: String?, sourceFrame: CGRect, sourceCornerRadius: CGFloat, router: AppRouter) { + self.router = router self.image = image self.imageDescription = description - var frame = sourceView.convert(sourceView.bounds, to: sourceViewController.view) - if let scrollView = sourceViewController.view as? UIScrollView { - let scale = scrollView.zoomScale - let width = frame.width * scale - let height = frame.height * scale - let x = frame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX - let y = frame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY - frame = CGRect(x: x, y: y, width: width, height: height) - } - self.originFrame = frame - self.originCornerRadius = sourceView.layer.cornerRadius - self.transitioningDelegate = sourceViewController + self.originFrame = sourceFrame + self.originCornerRadius = sourceCornerRadius + + super.init(nibName: "LargeImageViewController", bundle: nil) } required init?(coder aDecoder: NSCoder) { @@ -206,7 +194,7 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate { } @IBAction func closeButtonPressed(_ sender: Any) { - delegate?.closeLargeImage() + dismiss(animated: true) } @IBAction func downloadPressed(_ sender: Any) { diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 7bd98b6c..50f40fa6 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -10,6 +10,8 @@ import UIKit class MainTabBarViewController: UITabBarController { + lazy var router = AppRouter(root: self) + override func viewDidLoad() { super.viewDidLoad() @@ -31,20 +33,20 @@ class MainTabBarViewController: UITabBarController { func createVC(for tab: Tab) -> UIViewController { switch tab { case .home: - return TimelineTableViewController(for: .home) + return TimelineTableViewController(for: .home, router: router) case .federated: - return TimelineTableViewController(for: .public(local: false)) + return TimelineTableViewController(for: .public(local: false), router: router) case .local: - return TimelineTableViewController(for: .public(local: true)) + return TimelineTableViewController(for: .public(local: true), router: router) case .myProfile: - let myProfile = ProfileTableViewController(accountID: nil) + let myProfile = ProfileTableViewController(accountID: nil, router: router) myProfile.title = "My Profile" MastodonController.getOwnAccount { (account) in myProfile.accountID = account.id } return myProfile case .notifications: - return embedInNavigationController(NotificationsTableViewController()) + return NotificationsTableViewController(router: router) case .preferences: return PreferencesTableViewController.create() } diff --git a/Tusker/Screens/Notifications/NotificationsTableViewController.swift b/Tusker/Screens/Notifications/NotificationsTableViewController.swift index 5fd4ffe7..a45444c9 100644 --- a/Tusker/Screens/Notifications/NotificationsTableViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsTableViewController.swift @@ -11,6 +11,8 @@ import Pachyderm class NotificationsTableViewController: UITableViewController { + let router: AppRouter + var notifications: [Pachyderm.Notification] = [] { didSet { DispatchQueue.main.async { @@ -22,8 +24,11 @@ class NotificationsTableViewController: UITableViewController { var newer: RequestRange? var older: RequestRange? - init() { + init(router: AppRouter) { + self.router = router + super.init(style: .plain) + self.title = "Notifications" self.refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshNotifications(_:)), for: .valueChanged) @@ -149,4 +154,3 @@ class NotificationsTableViewController: UITableViewController { } extension NotificationsTableViewController: StatusTableViewCellDelegate {} -extension NotificationsTableViewController: LargeImageViewControllerDelegate {} diff --git a/Tusker/Screens/Profile/ProfileTableViewController.swift b/Tusker/Screens/Profile/ProfileTableViewController.swift index 1d9b5aba..e8cd477f 100644 --- a/Tusker/Screens/Profile/ProfileTableViewController.swift +++ b/Tusker/Screens/Profile/ProfileTableViewController.swift @@ -12,6 +12,8 @@ import SafariServices class ProfileTableViewController: UITableViewController, PreferencesAdaptive { + let router: AppRouter + var accountID: String! { didSet { if shouldLoadOnAccountIDSet { @@ -36,9 +38,12 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { var shouldLoadOnAccountIDSet = false var loadingVC: LoadingViewController? = nil - init(accountID: String?) { - super.init(style: .plain) + init(accountID: String?, router: AppRouter) { self.accountID = accountID + self.router = router + + super.init(style: .plain) + self.refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: #selector(refreshStatuses(_:)), for: .valueChanged) navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composePressed(_:))) @@ -107,7 +112,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { var actions = [UIPreviewActionItem]() if let account = MastodonCache.account(for: accountID) { actions.append(UIPreviewAction(title: "Open in Safari", style: .default, handler: { (_, _) in - let vc = self.viewController(forURL: account.url) + let vc = self.router.safariViewController(for: account.url) UIApplication.shared.delegate!.window!!.rootViewController!.present(vc, animated: true) })) actions.append(UIPreviewAction(title: "Share", style: .default, handler: { (_, _) in @@ -115,7 +120,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { UIApplication.shared.delegate!.window!!.rootViewController!.present(vc, animated: true) })) actions.append(UIPreviewAction(title: "Send Message", style: .default, handler: { (_, _) in - let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) + let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, router: self.router)) UIApplication.shared.delegate!.window!!.rootViewController!.present(vc, animated: true) })) } @@ -148,7 +153,7 @@ class ProfileTableViewController: UITableViewController, PreferencesAdaptive { func sendMessageMentioning() { guard let account = MastodonCache.account(for: accountID) else { fatalError("Missing cached account \(accountID!)") } - let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct)) + let vc = UINavigationController(rootViewController: ComposeViewController(mentioningAcct: account.acct, router: router)) present(vc, animated: true) } @@ -279,4 +284,3 @@ extension ProfileTableViewController: ProfileHeaderTableViewCellDelegate { } } } -extension ProfileTableViewController: LargeImageViewControllerDelegate {} diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index 4f410abb..29b26b3b 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -11,6 +11,8 @@ import Pachyderm class TimelineTableViewController: UITableViewController { + let router: AppRouter + lazy var favoriteActionImage: UIImage = UIGraphicsImageRenderer(size: CGSize(width: 30 * 137/131, height: 30)).image { _ in UIImage(named: "Favorite")!.draw(in: CGRect(x: 0, y: 0, width: 30 * 137/131, height: 30)) } @@ -37,9 +39,11 @@ class TimelineTableViewController: UITableViewController { var newer: RequestRange? var older: RequestRange? - init(for timeline: Timeline) { - super.init(style: .plain) + init(for timeline: Timeline, router: AppRouter) { self.timeline = timeline + self.router = router + + super.init(style: .plain) switch timeline { case .home: @@ -168,4 +172,3 @@ class TimelineTableViewController: UITableViewController { } extension TimelineTableViewController: StatusTableViewCellDelegate {} -extension TimelineTableViewController: LargeImageViewControllerDelegate {} diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index a96bd4ab..c30e1542 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -12,15 +12,9 @@ import Pachyderm class UserActivityManager { // MARK: - Utils - private static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { - UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: animated, completion: completion) - } - - private static func presentNav(_ vc: UIViewController, animated: Bool) { - let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController - let navController = tabBarController.selectedViewController as! UINavigationController - navController.pushViewController(vc, animated: animated) - } + private static var router: AppRouter = { + return (UIApplication.shared.delegate as! AppDelegate).router + }() // MARK: - New Post static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity { @@ -41,7 +35,7 @@ class UserActivityManager { static func handleNewPost(activity: NSUserActivity) { // TODO: check not currently showing compose screen let mentioning = activity.userInfo?["mentioning"] as? String - presentModally(UINavigationController(rootViewController: ComposeViewController(mentioningAcct: mentioning)), animated: true) + router.present(router.compose(mentioningAcct: mentioning), animated: true) } // MARK: - Check Notifications diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 3d74c8da..47d3e0cf 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -11,33 +11,26 @@ import SafariServices import Pachyderm protocol TuskerNavigationDelegate { - func viewController(forAccount accountID: String) -> UIViewController + + var router: AppRouter { get } func selected(account accountID: String) - func viewController(forMention mention: Mention) -> UIViewController - func selected(mention: Mention) - func viewController(forTag tag: Hashtag) -> UIViewController - func selected(tag: Hashtag) - func viewController(forURL url: URL) -> UIViewController - func selected(url: URL) - func viewController(forStatus statusID: String) -> UIViewController - func selected(status statusID: String) func compose() func reply(to statusID: String) - func viewController(forImage image: UIImage, description: String?, animatingFrom originView: UIView) -> UIViewController + func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController - func showLargeImage(_ image: UIImage, description: String?, animatingFrom originView: UIView) + func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) func showMoreOptions(forStatus statusID: String) @@ -46,10 +39,6 @@ protocol TuskerNavigationDelegate { extension TuskerNavigationDelegate where Self: UIViewController { - func viewController(forAccount accountID: String) -> UIViewController { - return ProfileTableViewController(accountID: accountID) - } - func selected(account accountID: String) { // don't open if the account is the same as the current one if let profileController = self as? ProfileTableViewController, @@ -57,49 +46,19 @@ extension TuskerNavigationDelegate where Self: UIViewController { return } - guard let navigationController = navigationController else { - fatalError("Can't show profile VC when not in navigation controller") - } - let vc = viewController(forAccount: accountID) - navigationController.pushViewController(vc, animated: true) - } - - func viewController(forMention mention: Mention) -> UIViewController { - return ProfileTableViewController(accountID: mention.id) + router.push(router.profile(for: accountID), animated: true) } func selected(mention: Mention) { - guard let navigationController = navigationController else { - fatalError("Can't show profile VC from mention when not in navigation controller") - } - let vc = viewController(forMention: mention) - navigationController.pushViewController(vc, animated: true) - } - - func viewController(forTag tag: Hashtag) -> UIViewController { - let timeline = Timeline.tag(hashtag: tag.name) - return TimelineTableViewController(for: timeline) + router.push(router.profile(for: mention), animated: true) } func selected(tag: Hashtag) { - guard let navigationController = navigationController else { - fatalError("Can't show hashtag timeline when not in navigation controller") - } - let vc = viewController(forTag: tag) - navigationController.pushViewController(vc, animated: true) - } - - func viewController(forURL url: URL) -> UIViewController { - return SFSafariViewController(url: url) + router.push(router.timeline(tag: tag), animated: true) } func selected(url: URL) { - let vc = viewController(forURL: url) - present(vc, animated: true) - } - - func viewController(forStatus statusID: String) -> UIViewController { - return ConversationTableViewController(for: statusID) + router.present(router.safariViewController(for: url), animated: true) } func selected(status statusID: String) { @@ -109,73 +68,43 @@ extension TuskerNavigationDelegate where Self: UIViewController { return } - guard let navigationController = navigationController else { - fatalError("Can't show conversation VC when not in navigation controller") - } - let vc = viewController(forStatus: statusID) - navigationController.pushViewController(vc, animated: true) + router.push(router.conversation(for: statusID), animated: true) } func compose() { - let vc = UINavigationController(rootViewController: ComposeViewController()) - present(vc, animated: true) + let vc: UINavigationController = UINavigationController(rootViewController: router.compose()) + router.present(vc, animated: true) } func reply(to statusID: String) { - let vc = UINavigationController(rootViewController: ComposeViewController(inReplyTo: statusID)) - present(vc, animated: true) + let vc = UINavigationController(rootViewController: router.compose(inReplyTo: statusID)) + router.present(vc, animated: true) } - func viewController(forImage image: UIImage, description: String?, animatingFrom originView: UIView) -> UIViewController { - guard let self = self as? UIViewController & LargeImageViewControllerDelegate else { - fatalError("Can't create large image view controller unless self is LargeImageViewControllerDelegate") + func largeImage(_ image: UIImage, description: String?, sourceView: UIView) -> LargeImageViewController { + var sourceFrame = sourceView.convert(sourceView.bounds, to: view) + if let scrollView = view as? UIScrollView { + let scale = scrollView.zoomScale + let width = sourceFrame.width * scale + let height = sourceFrame.height * scale + let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX + let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY + sourceFrame = CGRect(x: x, y: y, width: width, height: height) } - let vc = LargeImageViewController(image: image, description: description, sourceView: originView, sourceViewController: self) - vc.delegate = self - return vc + let sourceCornerRadius = sourceView.layer.cornerRadius + return router.largeImage(image, description: description, sourceFrame: sourceFrame, sourceCornerRadius: sourceCornerRadius, transitioningDelegate: self) } - func showLargeImage(_ image: UIImage, description: String?, animatingFrom originView: UIView) { - let vc = viewController(forImage: image, description: description, animatingFrom: originView) - present(vc, animated: true) + func showLargeImage(_ image: UIImage, description: String?, animatingFrom sourceView: UIView) { + router.present(largeImage(image, description: description, sourceView: sourceView), animated: true) } func showMoreOptions(forStatus statusID: String) { - guard let status = MastodonCache.status(for: statusID) else { fatalError("Missing cached status \(statusID)") } - - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - if let url = status.url { - alert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { (_) in - let vc = SFSafariViewController(url: url) - self.present(vc, animated: true) - })) - alert.addAction(UIAlertAction(title: "Copy", style: .default, handler: { (_) in - UIPasteboard.general.url = url - })) - alert.addAction(UIAlertAction(title: "Share...", style: .default, handler: { (_) in - let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil) - self.present(vc, animated: true) - })) - } - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true) + router.present(router.moreOptions(forStatus: statusID), animated: true) } func showMoreOptions(forURL url: URL) { - let alert = UIAlertController(title: nil, message: url.absoluteString, preferredStyle: .actionSheet) - alert.addAction(UIAlertAction(title: "Open in Safari", style: .default, handler: { (_) in - let vc = SFSafariViewController(url: url) - self.present(vc, animated: true) - })) - alert.addAction(UIAlertAction(title: "Copy", style: .default, handler: { (_) in - UIPasteboard.general.url = url - })) - alert.addAction(UIAlertAction(title: "Share...", style: .default, handler: { (_) in - let vc = UIActivityViewController(activityItems: [url], applicationActivities: nil) - self.present(vc, animated: true) - })) - alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - present(alert, animated: true) + router.present(router.moreOptions(forURL: url), animated: true) } } diff --git a/Tusker/Views/ContentLabel.swift b/Tusker/Views/ContentLabel.swift index 74c301cd..cb5b072b 100644 --- a/Tusker/Views/ContentLabel.swift +++ b/Tusker/Views/ContentLabel.swift @@ -92,11 +92,11 @@ class ContentLabel: TTTAttributedLabel { } let text = (self.text as! NSString).substring(with: link.result.range) if let mention = getMention(for: url, text: text) { - return navigationDelegate.viewController(forMention: mention) + return navigationDelegate.router.profile(for: mention) } else if let tag = getHashtag(for: url, text: text) { - return navigationDelegate.viewController(forTag: tag) + return navigationDelegate.router.timeline(tag: tag) } else { - return navigationDelegate.viewController(forURL: url) + return navigationDelegate.router.safariViewController(for: url) } } diff --git a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift index 048c437b..5cf7dcdb 100644 --- a/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/ActionNotificationTableViewCell.swift @@ -193,11 +193,11 @@ class ActionNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { extension ActionNotificationTableViewCell: PreviewViewControllerProvider { func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { if avatarContainerView.frame.contains(location) { - return delegate?.viewController(forAccount: notification.account.id) + return delegate?.router.profile(for: notification.account.id) } else if contentLabel.frame.contains(location), let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { return vc } - return delegate?.viewController(forStatus: statusID) + return delegate?.router.conversation(for: statusID) } } diff --git a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift index 4c66702a..fa08623f 100644 --- a/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift +++ b/Tusker/Views/Notifications/FollowNotificationTableViewCell.swift @@ -11,6 +11,8 @@ import Pachyderm class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { + var router: AppRouter! + var delegate: StatusTableViewCellDelegate? @IBOutlet weak var followLabel: UILabel! @@ -99,6 +101,6 @@ class FollowNotificationTableViewCell: UITableViewCell, PreferencesAdaptive { extension FollowNotificationTableViewCell: PreviewViewControllerProvider { func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { - return delegate?.viewController(forAccount: accountID) + return router.profile(for: accountID) } } diff --git a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift index 056789d7..d107ccfd 100644 --- a/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift +++ b/Tusker/Views/Status/ConversationMainStatusTableViewCell.swift @@ -228,13 +228,13 @@ extension ConversationMainStatusTableViewCell: AttachmentViewDelegate { extension ConversationMainStatusTableViewCell: PreviewViewControllerProvider { func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { if avatarImageView.frame.contains(location) { - return delegate?.viewController(forAccount: accountID) + return delegate?.router.profile(for: 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?.viewController(forImage: image, description: description, animatingFrom: attachmentView) + return delegate?.largeImage(image, description: description, sourceView: attachmentView) } } else if contentLabel.frame.contains(location), let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { diff --git a/Tusker/Views/Status/StatusTableViewCell.swift b/Tusker/Views/Status/StatusTableViewCell.swift index 24bc1c20..86e36193 100644 --- a/Tusker/Views/Status/StatusTableViewCell.swift +++ b/Tusker/Views/Status/StatusTableViewCell.swift @@ -357,18 +357,18 @@ extension StatusTableViewCell: AttachmentViewDelegate { extension StatusTableViewCell: PreviewViewControllerProvider { func getPreviewViewController(forLocation location: CGPoint, sourceViewController: UIViewController) -> UIViewController? { if avatarImageView.frame.contains(location) { - return delegate?.viewController(forAccount: accountID) + return delegate?.router.profile(for: 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?.viewController(forImage: image, description: description, animatingFrom: attachmentView) + return delegate?.largeImage(image, description: description, sourceView: attachmentView) } } else if contentLabel.frame.contains(location), let vc = contentLabel.getViewController(forLinkAt: contentLabel.convert(location, from: self)) { return vc } - return delegate?.viewController(forStatus: statusID) + return delegate?.router.conversation(for: statusID) } } diff --git a/Tusker/XCallbackURL/XCBActions.swift b/Tusker/XCallbackURL/XCBActions.swift index b947af94..8741d520 100644 --- a/Tusker/XCallbackURL/XCBActions.swift +++ b/Tusker/XCallbackURL/XCBActions.swift @@ -13,15 +13,9 @@ import SwiftSoup struct XCBActions { // MARK: - Utils - private static func presentModally(_ vc: UIViewController, animated: Bool, completion: (() -> Void)? = nil) { - UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: animated, completion: completion) - } - - private static func presentNav(_ vc: UIViewController, animated: Bool) { - let tabBarController = UIApplication.shared.keyWindow!.rootViewController! as! UITabBarController - let navController = tabBarController.selectedViewController as! UINavigationController - navController.pushViewController(vc, animated: animated) - } + private static var router: AppRouter = { + return (UIApplication.shared.delegate as! AppDelegate).router + }() private static func getStatus(from request: XCBRequest, session: XCBSession, completion: @escaping (Status) -> Void) { if let id = request.arguments["statusID"] { @@ -111,9 +105,9 @@ struct XCBActions { // MARK: - Statuses static func showStatus(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { getStatus(from: request, session: session) { (status) in - let vc = ConversationTableViewController(for: status.id) + let vc = router.conversation(for: status.id) DispatchQueue.main.async { - presentNav(vc, animated: true) + router.push(vc, animated: true) } } } @@ -146,10 +140,10 @@ struct XCBActions { } } } else { - let compose = ComposeViewController(mentioningAcct: mentioning, text: text) + let compose = router.compose(mentioningAcct: mentioning, text: text) compose.xcbSession = session let vc = UINavigationController(rootViewController: compose) - presentModally(vc, animated: true) + router.present(vc, animated: true) } } @@ -213,9 +207,9 @@ struct XCBActions { if silent ?? false { performAction(status: status, completion: nil) } else { - let vc = ConversationTableViewController(for: status.id) + let vc = router.conversation(for: status.id) DispatchQueue.main.async { - presentNav(vc, animated: true) + router.push(vc, animated: true) } let alertController = UIAlertController(title: alertTitle, message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in @@ -229,7 +223,7 @@ struct XCBActions { session.complete(with: .cancel) })) DispatchQueue.main.async { - presentModally(alertController, animated: true) + router.present(alertController, animated: true) } } } @@ -240,9 +234,9 @@ struct XCBActions { // MARK: - Accounts static func showAccount(_ request: XCBRequest, _ session: XCBSession, _ silent: Bool?) { getAccount(from: request, session: session) { (account) in - let vc = ProfileTableViewController(accountID: account.id) + let vc = router.profile(for: account.id) DispatchQueue.main.async { - presentNav(vc, animated: true) + router.push(vc, animated: true) } } } @@ -297,9 +291,9 @@ struct XCBActions { if silent ?? false { performAction(account) } else { - let vc = ProfileTableViewController(accountID: account.id) + let vc = router.profile(for: account.id) DispatchQueue.main.async { - presentNav(vc, animated: true) + router.push(vc, animated: true) } let alertController = UIAlertController(title: "Follow \(account.realDisplayName)?", message: nil, preferredStyle: .alert) alertController.addAction(UIAlertAction(title: "Ok", style: .default, handler: { (_) in @@ -309,7 +303,7 @@ struct XCBActions { session.complete(with: .cancel) })) DispatchQueue.main.async { - presentModally(alertController, animated: true) + router.present(alertController, animated: true) } } }