From 60d9d3537d9a98ff7ed15dc34d8761231f76539e Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Thu, 17 Nov 2022 10:22:13 +0100 Subject: [PATCH] feat: Implement double-tap account switching on iPad --- Mastodon.xcodeproj/project.pbxproj | 4 ++ .../Extension/AppContext+NextAccount.swift | 47 +++++++++++++++++++ .../Root/ContentSplitViewController.swift | 13 +++++ .../Root/MainTab/MainTabBarController.swift | 25 +--------- .../Root/Sidebar/SidebarViewController.swift | 23 +++++++++ 5 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 Mastodon/Extension/AppContext+NextAccount.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5724244b6..d8020fb3f 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FB3D33725E6401400AAD544 /* PickServerCell.swift */; }; 164F0EBC267D4FE400249499 /* BoopSound.caf in Resources */ = {isa = PBXBuildFile; fileRef = 164F0EBB267D4FE400249499 /* BoopSound.caf */; }; 18BC7629F65E6DB12CB8416D /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */; }; + 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */; }; 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198642261BF09500F0B013 /* SearchResultItem.swift */; }; 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D198648261C0B8500F0B013 /* SearchResultSection.swift */; }; 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; @@ -518,6 +519,7 @@ 0FB3D33725E6401400AAD544 /* PickServerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerCell.swift; sourceTree = ""; }; 164F0EBB267D4FE400249499 /* BoopSound.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = BoopSound.caf; sourceTree = ""; }; 1D6D967E77A5357E2C6110D9 /* Pods-Mastodon.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk - debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk - debug.xcconfig"; sourceTree = ""; }; + 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppContext+NextAccount.swift"; sourceTree = ""; }; 2D198642261BF09500F0B013 /* SearchResultItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultItem.swift; sourceTree = ""; }; 2D198648261C0B8500F0B013 /* SearchResultSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultSection.swift; sourceTree = ""; }; 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; @@ -2222,6 +2224,7 @@ isa = PBXGroup; children = ( 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, + 2A82294E29262EE000D2A1F7 /* AppContext+NextAccount.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */, @@ -3465,6 +3468,7 @@ DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */, 2D198649261C0B8500F0B013 /* SearchResultSection.swift in Sources */, DB4F097B26A039FF00D62E92 /* SearchHistorySection.swift in Sources */, + 2A82294F29262EE000D2A1F7 /* AppContext+NextAccount.swift in Sources */, DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, DB9F58EC26EF435000E7BBE9 /* AccountViewController.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, diff --git a/Mastodon/Extension/AppContext+NextAccount.swift b/Mastodon/Extension/AppContext+NextAccount.swift new file mode 100644 index 000000000..a8eae1e13 --- /dev/null +++ b/Mastodon/Extension/AppContext+NextAccount.swift @@ -0,0 +1,47 @@ +// +// AppContext+NextAccount.swift +// Mastodon +// +// Created by Marcus Kida on 17.11.22. +// + +import CoreData +import CoreDataStack +import MastodonCore +import MastodonSDK + +extension AppContext { + func nextAccount(in authContext: AuthContext) -> MastodonAuthentication? { + let request = MastodonAuthentication.sortedFetchRequest + guard + let accounts = try? managedObjectContext.fetch(request), + accounts.count > 1 + else { return nil } + + let nextSelectedAccountIndex: Int? = { + for (index, account) in accounts.enumerated() { + guard account == authContext.mastodonAuthenticationBox + .authenticationRecord + .object(in: managedObjectContext) + else { continue } + + let nextAccountIndex = index + 1 + + if accounts.count > nextAccountIndex { + return nextAccountIndex + } else { + return 0 + } + } + + return nil + }() + + guard + let nextSelectedAccountIndex = nextSelectedAccountIndex, + accounts.count > nextSelectedAccountIndex + else { return nil } + + return accounts[nextSelectedAccountIndex] + } +} diff --git a/Mastodon/Scene/Root/ContentSplitViewController.swift b/Mastodon/Scene/Root/ContentSplitViewController.swift index 3f4758e8e..a10f0ed9b 100644 --- a/Mastodon/Scene/Root/ContentSplitViewController.swift +++ b/Mastodon/Scene/Root/ContentSplitViewController.swift @@ -124,4 +124,17 @@ extension ContentSplitViewController: SidebarViewControllerDelegate { accountListViewController.preferredContentSize = CGSize(width: 375, height: 400) } + func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView) { + guard case let .tab(tab) = item, tab == .me else { return } + guard let authContext = authContext else { return } + assert(Thread.isMainThread) + + guard let nextAccount = context.nextAccount(in: authContext) else { return } + + Task { @MainActor in + let isActive = try await context.authenticationService.activeMastodonUser(domain: nextAccount.domain, userID: nextAccount.userID) + guard isActive else { return } + self.coordinator.setup() + } + } } diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 5f77d9584..5143e00c9 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -406,30 +406,7 @@ extension MainTabBarController { guard let authContext = authContext else { return } assert(Thread.isMainThread) - let request = MastodonAuthentication.sortedFetchRequest - guard let accounts = try? context.managedObjectContext.fetch(request), accounts.count > 1 else { return } - - let nextSelectedAccountIndex: Int? = { - for (index, account) in accounts.enumerated() { - guard account == authContext.mastodonAuthenticationBox - .authenticationRecord - .object(in: context.managedObjectContext) - else { continue } - - let nextAccountIndex = index + 1 - - if accounts.count > nextAccountIndex { - return nextAccountIndex - } else { - return 0 - } - } - - return nil - }() - - guard let nextSelectedAccountIndex = nextSelectedAccountIndex, accounts.count > nextSelectedAccountIndex else { return } - let nextAccount = accounts[nextSelectedAccountIndex] + guard let nextAccount = context.nextAccount(in: authContext) else { return } Task { @MainActor in let isActive = try await context.authenticationService.activeMastodonUser(domain: nextAccount.domain, userID: nextAccount.userID) diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 70e1239b6..c1faafca7 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -15,6 +15,7 @@ import MastodonUI protocol SidebarViewControllerDelegate: AnyObject { func sidebarViewController(_ sidebarViewController: SidebarViewController, didSelectTab tab: MainTabBarController.Tab) func sidebarViewController(_ sidebarViewController: SidebarViewController, didLongPressItem item: SidebarViewModel.Item, sourceView: UIView) + func sidebarViewController(_ sidebarViewController: SidebarViewController, didDoubleTapItem item: SidebarViewModel.Item, sourceView: UIView) } final class SidebarViewController: UIViewController, NeedsDependency { @@ -143,6 +144,14 @@ extension SidebarViewController { let sidebarLongPressGestureRecognizer = UILongPressGestureRecognizer() sidebarLongPressGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarLongPressGestureRecognizerHandler(_:))) collectionView.addGestureRecognizer(sidebarLongPressGestureRecognizer) + + let sidebarDoubleTapGestureRecognizer = UITapGestureRecognizer() + sidebarDoubleTapGestureRecognizer.numberOfTapsRequired = 2 + sidebarDoubleTapGestureRecognizer.addTarget(self, action: #selector(SidebarViewController.sidebarDoubleTapGestureRecognizerHandler(_:))) + sidebarDoubleTapGestureRecognizer.delaysTouchesBegan = true + sidebarDoubleTapGestureRecognizer.cancelsTouchesInView = true + collectionView.addGestureRecognizer(sidebarDoubleTapGestureRecognizer) + } private func setupBackground(theme: Theme) { @@ -176,6 +185,20 @@ extension SidebarViewController { guard let cell = collectionView.cellForItem(at: indexPath) else { return } delegate?.sidebarViewController(self, didLongPressItem: item, sourceView: cell) } + + @objc private func sidebarDoubleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + guard sender.state == .ended else { return } + + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + assert(sender.view === collectionView) + + let position = sender.location(in: collectionView) + guard let indexPath = collectionView.indexPathForItem(at: position) else { return } + guard let diffableDataSource = viewModel.diffableDataSource else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + guard let cell = collectionView.cellForItem(at: indexPath) else { return } + delegate?.sidebarViewController(self, didDoubleTapItem: item, sourceView: cell) + } }