From 84ed9e92ee788a8816b4ff9ffcd2af6df1beef73 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 3 Mar 2025 12:40:41 -0500 Subject: [PATCH] Remove catchNSException and instead swizzle things in Objective C Apparently any Swift stack frames between throwing/catching an NSException can break things. --- Tusker.xcodeproj/project.pbxproj | 10 +++-- Tusker/AppDelegate.swift | 36 +++------------- Tusker/Scenes/AuxiliarySceneDelegate.swift | 4 ++ Tusker/Scenes/ComposeSceneDelegate.swift | 4 ++ Tusker/Scenes/MainSceneDelegate.swift | 4 ++ Tusker/Scenes/TuskerSceneDelegate.swift | 22 +++------- .../AccountFollowsListViewController.swift | 4 +- .../AccountListViewController.swift | 4 +- ...ConversationCollectionViewController.swift | 4 +- .../ConversationViewController.swift | 6 +-- .../TrendingStatusesViewController.swift | 4 +- ...LocalPredicateStatusesViewController.swift | 4 +- ...ountSwitchingContainerViewController.swift | 4 +- .../Main/BaseMainTabBarViewController.swift | 8 ++-- Tusker/Screens/Main/Duckable+Root.swift | 11 +++-- .../Main/MainSplitViewController.swift | 8 ++-- .../Main/TuskerRootViewController.swift | 2 +- ...lowRequestNotificationViewController.swift | 4 +- ...otificationsCollectionViewController.swift | 4 +- .../ProfileStatusesViewController.swift | 4 +- .../Profile/ProfileViewController.swift | 6 +-- ...nAccountListCollectionViewController.swift | 4 +- .../Timeline/TimelineViewController.swift | 4 +- .../AdaptableNavigationController.swift | 6 +-- .../EnhancedNavigationViewController.swift | 8 ++-- .../SegmentedPageViewController.swift | 19 ++++----- .../Utilities/SplitNavigationController.swift | 10 ++--- .../StatusBarTappableViewController.swift | 14 ------- Tusker/Swizzler.h | 42 +++++++++++++++++++ Tusker/Swizzler.m | 37 ++++++++++++++++ Tusker/Tusker-Bridging-Header.h | 11 +---- 31 files changed, 175 insertions(+), 137 deletions(-) delete mode 100644 Tusker/Screens/Utilities/StatusBarTappableViewController.swift create mode 100644 Tusker/Swizzler.h create mode 100644 Tusker/Swizzler.m diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 1e8d41926a..32d7ea9f25 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -113,7 +113,6 @@ D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D63CC701290EC0B8000E19DE /* Sentry */; }; D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; }; D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */; }; - D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */; }; D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; }; @@ -162,6 +161,7 @@ D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */; }; D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */; }; + D6606A102D761BA3004BBEF4 /* Swizzler.m in Sources */ = {isa = PBXBuildFile; fileRef = D6606A0F2D761BA3004BBEF4 /* Swizzler.m */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; }; D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; @@ -542,7 +542,6 @@ D63CC703290EC472000E19DE /* Dist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dist.xcconfig; sourceTree = ""; }; D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerSceneDelegate.swift; sourceTree = ""; }; D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = ""; }; - D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.swift; sourceTree = ""; }; D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = ""; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = ""; }; D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = ""; }; @@ -595,6 +594,8 @@ D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D65F613723AFD65D00F3CFD3 /* Embassy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Embassy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D6606A0E2D761BA3004BBEF4 /* Swizzler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Swizzler.h; sourceTree = ""; }; + D6606A0F2D761BA3004BBEF4 /* Swizzler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Swizzler.m; sourceTree = ""; }; D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = ""; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = ""; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = ""; }; @@ -1542,7 +1543,6 @@ D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */, - D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, @@ -1606,6 +1606,8 @@ D60088F02980D938005B4D00 /* Tusker.storekit */, D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */, D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */, + D6606A0E2D761BA3004BBEF4 /* Swizzler.h */, + D6606A0F2D761BA3004BBEF4 /* Swizzler.m */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */, D6D79F582A13293200AB2315 /* BackgroundManager.swift */, @@ -2157,7 +2159,6 @@ 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */, - D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */, D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */, D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */, @@ -2215,6 +2216,7 @@ D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */, D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, + D6606A102D761BA3004BBEF4 /* Swizzler.m in Sources */, D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, D68245122BCA1F4000AFB38B /* NotificationLoadingViewController.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 5778348b81..2e359fec51 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -30,7 +30,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { configureSentry() #endif #if !os(visionOS) - swizzleStatusBar() + let swizzledStatusBar = Swizzler.swizzleStatusBarManager(exceptionHandler: { + SentrySDK.capture(exception: $0) + }) + if !swizzledStatusBar { + Logging.general.error("Unable to swizzle status bar manager") + } swizzlePresentationController() #endif @@ -202,35 +207,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } #if !os(visionOS) - private func swizzleStatusBar() { - let selector = Selector(("handleTapAction:")) - var originalIMP: IMP? - let imp = imp_implementationWithBlock({ (self: UIStatusBarManager, sender: AnyObject) in - let original = unsafeBitCast(originalIMP!, to: (@convention(c) (UIStatusBarManager, Selector, AnyObject) -> Void).self) - let exception = catchNSException { - guard let windowScene = self.perform(Selector(("windowScene"))).takeUnretainedValue() as? UIWindowScene, - let xPosition = sender.value(forKey: "xPosition") as? CGFloat, - let delegate = windowScene.delegate as? TuskerSceneDelegate else { - original(self, selector, sender) - return - } - switch delegate.handleStatusBarTapped(xPosition: xPosition) { - case .stop: - return - case .continue: - original(self, selector, sender) - } - } - if let exception { - SentrySDK.capture(exception: exception) - } - } as @convention(block) (UIStatusBarManager, AnyObject) -> Void) - originalIMP = class_replaceMethod(UIStatusBarManager.self, selector, imp, "v@:@") - if originalIMP == nil { - Logging.general.error("Unable to swizzle status bar manager") - } - } - @available(iOS, obsoleted: 17.0) private func swizzlePresentationController() { guard #unavailable(iOS 17.0) else { diff --git a/Tusker/Scenes/AuxiliarySceneDelegate.swift b/Tusker/Scenes/AuxiliarySceneDelegate.swift index 2b48bde00e..45e0f0b29a 100644 --- a/Tusker/Scenes/AuxiliarySceneDelegate.swift +++ b/Tusker/Scenes/AuxiliarySceneDelegate.swift @@ -130,4 +130,8 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel @objc private func themePrefChanged() { applyAppearancePreferences() } + + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + doHandleStatusBarTapped(at: xPosition) + } } diff --git a/Tusker/Scenes/ComposeSceneDelegate.swift b/Tusker/Scenes/ComposeSceneDelegate.swift index c772a52aaf..c2a9b98c7d 100644 --- a/Tusker/Scenes/ComposeSceneDelegate.swift +++ b/Tusker/Scenes/ComposeSceneDelegate.swift @@ -113,6 +113,10 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg applyAppearancePreferences() } + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + doHandleStatusBarTapped(at: xPosition) + } + } extension ComposeSceneDelegate: ComposeHostingControllerDelegate { diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 31e5b6678d..5aae339833 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -310,6 +310,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate preferencesVC?.navigationState.showNotificationPreferences = true } + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + doHandleStatusBarTapped(at: xPosition) + } + } extension MainSceneDelegate: OnboardingViewControllerDelegate { diff --git a/Tusker/Scenes/TuskerSceneDelegate.swift b/Tusker/Scenes/TuskerSceneDelegate.swift index 16b77be5d0..fbe4eee0f6 100644 --- a/Tusker/Scenes/TuskerSceneDelegate.swift +++ b/Tusker/Scenes/TuskerSceneDelegate.swift @@ -12,21 +12,16 @@ import Sentry #endif @MainActor -protocol TuskerSceneDelegate: UISceneDelegate { +protocol TuskerSceneDelegate: UISceneDelegate, StatusBarTapHandling { var window: UIWindow? { get } var rootViewController: TuskerRootViewController? { get } } -enum StatusBarTapActionResult { - case `continue` - case stop -} - extension TuskerSceneDelegate { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + func doHandleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { if let rootViewController { let converted = rootViewController.view.convert(CGPoint(x: xPosition, y: 0), from: nil) - return rootViewController.handleStatusBarTapped(xPosition: converted.x) + return rootViewController.handleStatusBarTapped(at: converted.x) } return .continue } @@ -41,14 +36,9 @@ extension TuskerSceneDelegate { if #available(iOS 17.0, *) { window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode } else { - let exception = catchNSException { - let key = ["Controller", "Presentation", "root", "_"].reversed().joined() - if let rootPresentationController = window.value(forKey: key) as? UIPresentationController { - rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode) - } - } - if let exception { - SentrySDK.capture(exception: exception) + let key = ["Controller", "Presentation", "root", "_"].reversed().joined() + if let rootPresentationController = window.value(forKey: key) as? UIPresentationController { + rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode) } } #endif diff --git a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift index 07cd76707a..e8e3f1de0d 100644 --- a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift +++ b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift @@ -297,8 +297,8 @@ extension AccountFollowsListViewController: MenuActionProvider { extension AccountFollowsListViewController: ToastableViewController { } -extension AccountFollowsListViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension AccountFollowsListViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Account List/AccountListViewController.swift b/Tusker/Screens/Account List/AccountListViewController.swift index f0b533dbd9..e25edbd1e2 100644 --- a/Tusker/Screens/Account List/AccountListViewController.swift +++ b/Tusker/Screens/Account List/AccountListViewController.swift @@ -132,8 +132,8 @@ extension AccountListViewController: TuskerNavigationDelegate { extension AccountListViewController: MenuActionProvider { } -extension AccountListViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension AccountListViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift index b3559e3023..87ec96dcb8 100644 --- a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift +++ b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift @@ -462,8 +462,8 @@ extension ConversationCollectionViewController: TabBarScrollableViewController { } } -extension ConversationCollectionViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension ConversationCollectionViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index 121870b754..5936e2d3b3 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -430,10 +430,10 @@ extension ConversationViewController: ToastableViewController { } } -extension ConversationViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension ConversationViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { if case .displaying(let vc) = state { - return vc.handleStatusBarTapped(xPosition: xPosition) + return vc.handleStatusBarTapped(at: xPosition) } else { return .continue } diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index db6e5cac5d..3901956bc8 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -267,8 +267,8 @@ extension TrendingStatusesViewController: StatusCollectionViewCellDelegate { } } -extension TrendingStatusesViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension TrendingStatusesViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift index 1d5081b296..39e268cd10 100644 --- a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift +++ b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift @@ -446,8 +446,8 @@ extension LocalPredicateStatusesViewController: TabBarScrollableViewController { } } -extension LocalPredicateStatusesViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension LocalPredicateStatusesViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index afdc000208..7c8c16a248 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -184,12 +184,12 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { return root.presentPreferences(completion: completion) } - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { loadViewIfNeeded() if root.isFastAccountSwitcherActive { return .stop } else { - return root.handleStatusBarTapped(xPosition: xPosition) + return root.handleStatusBarTapped(at: xPosition) } } } diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index b5cd951c00..75d966faeb 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -198,12 +198,12 @@ extension BaseMainTabBarViewController: BackgroundableViewController { } } -extension BaseMainTabBarViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension BaseMainTabBarViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { guard presentedViewController == nil, - let vc = selectedViewController as? StatusBarTappableViewController else { + let vc = selectedViewController as? StatusBarTapHandling else { return .continue } - return vc.handleStatusBarTapped(xPosition: xPosition) + return vc.handleStatusBarTapped(at: xPosition) } } diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 1dd46dda47..c831b43314 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -12,6 +12,13 @@ import UIKit import Duckable import ComposeUI +@available(iOS 16.0, *) +extension DuckableContainerViewController: @retroactive StatusBarTapHandling { + public func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + (child as? TuskerRootViewController)?.handleStatusBarTapped(at: xPosition) ?? .continue + } +} + @available(iOS 16.0, *) extension DuckableContainerViewController: AccountSwitchableViewController { func stateRestorationActivity() -> NSUserActivity? { @@ -48,10 +55,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as? TuskerRootViewController)?.presentPreferences(completion: completion) } - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - (child as? TuskerRootViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue - } - var isFastAccountSwitcherActive: Bool { (child as? AccountSwitchableViewController)?.isFastAccountSwitcherActive ?? false } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index e1c37957ff..f9b37ebc3d 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -648,17 +648,17 @@ extension MainSplitViewController: TuskerRootViewController { return vc } - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { guard presentedViewController == nil else { return .continue } if traitCollection.horizontalSizeClass == .compact { - return tabBarViewController.handleStatusBarTapped(xPosition: xPosition) + return tabBarViewController.handleStatusBarTapped(at: xPosition) } else { let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view) if secondaryNavController.view.bounds.contains(pointInSecondary), - let statusBarTappable = secondaryNavController as? StatusBarTappableViewController { - return statusBarTappable.handleStatusBarTapped(xPosition: pointInSecondary.x) + let statusBarTappable = secondaryNavController as? StatusBarTapHandling { + return statusBarTappable.handleStatusBarTapped(at: pointInSecondary.x) } else { return .continue } diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index c779648c1d..661d068a72 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -10,7 +10,7 @@ import UIKit import ComposeUI @MainActor -protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { +protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTapHandling { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) func getNavigationDelegate() -> TuskerNavigationDelegate? diff --git a/Tusker/Screens/Notifications/FollowRequestNotificationViewController.swift b/Tusker/Screens/Notifications/FollowRequestNotificationViewController.swift index 5229470275..eb1a77bf1b 100644 --- a/Tusker/Screens/Notifications/FollowRequestNotificationViewController.swift +++ b/Tusker/Screens/Notifications/FollowRequestNotificationViewController.swift @@ -136,8 +136,8 @@ extension FollowRequestNotificationViewController: TuskerNavigationDelegate { extension FollowRequestNotificationViewController: MenuActionProvider { } -extension FollowRequestNotificationViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension FollowRequestNotificationViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index eb042b7240..fe2c653b7c 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -803,8 +803,8 @@ extension NotificationsCollectionViewController: TabBarScrollableViewController } } -extension NotificationsCollectionViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension NotificationsCollectionViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index 802b9da37f..5799b1b3c8 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -690,8 +690,8 @@ extension ProfileStatusesViewController: TabBarScrollableViewController { } } -extension ProfileStatusesViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension ProfileStatusesViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index cc5c45fb05..6023054446 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -390,10 +390,10 @@ extension ProfileViewController: TabBarScrollableViewController { } } -extension ProfileViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension ProfileViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { guard isViewLoaded else { return .stop } - return currentViewController.handleStatusBarTapped(xPosition: xPosition) + return currentViewController.handleStatusBarTapped(at: xPosition) } } diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift index da96e3947d..52a13cf801 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift @@ -425,8 +425,8 @@ extension StatusActionAccountListCollectionViewController: StatusCollectionViewC } } -extension StatusActionAccountListCollectionViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension StatusActionAccountListCollectionViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 4e4f34c0e6..e324fadff0 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -1437,8 +1437,8 @@ extension TimelineViewController: TabBarScrollableViewController { } } -extension TimelineViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension TimelineViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { collectionView.scrollToTop() return .stop } diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift index f991570f65..03f38f3384 100644 --- a/Tusker/Screens/Utilities/AdaptableNavigationController.swift +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -160,8 +160,8 @@ extension AdaptableNavigationController: BackgroundableViewController { } @available(iOS 17.0, *) -extension AdaptableNavigationController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - (topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue +extension AdaptableNavigationController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + (topViewController as? StatusBarTapHandling)?.handleStatusBarTapped(at: xPosition) ?? .continue } } diff --git a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift index ee74cb1353..ea131b28d0 100644 --- a/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift +++ b/Tusker/Screens/Utilities/EnhancedNavigationViewController.swift @@ -259,10 +259,10 @@ extension EnhancedNavigationViewController: BackgroundableViewController { } } -extension EnhancedNavigationViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - if let topVC = topViewController as? StatusBarTappableViewController { - return topVC.handleStatusBarTapped(xPosition: xPosition) +extension EnhancedNavigationViewController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + if let topVC = topViewController as? StatusBarTapHandling { + return topVC.handleStatusBarTapped(at: xPosition) } return .continue } diff --git a/Tusker/Screens/Utilities/SegmentedPageViewController.swift b/Tusker/Screens/Utilities/SegmentedPageViewController.swift index e05b46ec6b..a73ee7855a 100644 --- a/Tusker/Screens/Utilities/SegmentedPageViewController.swift +++ b/Tusker/Screens/Utilities/SegmentedPageViewController.swift @@ -12,7 +12,7 @@ protocol SegmentedPageViewControllerPage: Hashable { var segmentedControlTitle: String { get } } -class SegmentedPageViewController: UIViewController, UIPageViewControllerDelegate, TabbedPageViewController { +class SegmentedPageViewController: UIViewController, UIPageViewControllerDelegate, TabbedPageViewController, StatusBarTapHandling { private(set) var pages: [Page]! private let pageProvider: (Page) -> UIViewController @@ -178,6 +178,14 @@ class SegmentedPageViewController: UIView guard currentIndex > 0 else { return } selectPage(pages[currentIndex - 1], animated: true) } + + // MARK: StatusBarTapHandling + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { + if let current = currentViewController as? StatusBarTapHandling { + return current.handleStatusBarTapped(at: xPosition) + } + return .continue + } } extension SegmentedPageViewController { @@ -204,15 +212,6 @@ extension SegmentedPageViewController: BackgroundableViewController { } } -extension SegmentedPageViewController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - if let current = currentViewController as? StatusBarTappableViewController { - return current.handleStatusBarTapped(xPosition: xPosition) - } - return .continue - } -} - extension SegmentedPageViewController: NestedResponderProvider { var innerResponder: UIResponder? { currentViewController diff --git a/Tusker/Screens/Utilities/SplitNavigationController.swift b/Tusker/Screens/Utilities/SplitNavigationController.swift index 28200572e9..02b26dfd42 100644 --- a/Tusker/Screens/Utilities/SplitNavigationController.swift +++ b/Tusker/Screens/Utilities/SplitNavigationController.swift @@ -270,18 +270,18 @@ extension SplitNavigationController: TabBarScrollableViewController { } } -extension SplitNavigationController: StatusBarTappableViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { +extension SplitNavigationController: StatusBarTapHandling { + func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult { let vcs = viewControllers if !canShowSecondaryNav || vcs.count < 2 { - return (vcs.last! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue + return (vcs.last! as? StatusBarTapHandling)?.handleStatusBarTapped(at: xPosition) ?? .continue } else { let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view) let positionInSecondary = secondaryNav.view.convert(CGPoint(x: xPosition, y: 0), from: view) if rootNav.view.bounds.contains(positionInRoot) { - return (rootNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue + return (rootNav.topViewController as? StatusBarTapHandling)?.handleStatusBarTapped(at: positionInRoot.x) ?? .continue } else if secondaryNav.view.bounds.contains(positionInSecondary) { - return (secondaryNav.topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: positionInRoot.x) ?? .continue + return (secondaryNav.topViewController as? StatusBarTapHandling)?.handleStatusBarTapped(at: positionInRoot.x) ?? .continue } } return .continue diff --git a/Tusker/Screens/Utilities/StatusBarTappableViewController.swift b/Tusker/Screens/Utilities/StatusBarTappableViewController.swift deleted file mode 100644 index 13f5cc17a6..0000000000 --- a/Tusker/Screens/Utilities/StatusBarTappableViewController.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// StatusBarTappableViewController.swift -// Tusker -// -// Created by Shadowfacts on 11/1/22. -// Copyright © 2022 Shadowfacts. All rights reserved. -// - -import UIKit - -@MainActor -protocol StatusBarTappableViewController: UIViewController { - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult -} diff --git a/Tusker/Swizzler.h b/Tusker/Swizzler.h new file mode 100644 index 0000000000..7733ec63c7 --- /dev/null +++ b/Tusker/Swizzler.h @@ -0,0 +1,42 @@ +// +// Swizzler.h +// Tusker +// +// Created by Shadowfacts on 3/3/25. +// Copyright © 2025 Shadowfacts. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#if !TARGET_OS_VISION +@interface Swizzler : NSObject + ++ (BOOL)swizzleStatusBarManagerWithExceptionHandler:(void (^)(NSException *))captureException; + +@end + +typedef NS_ENUM(NSUInteger, StatusBarTapResult) { + StatusBarTapResultContinue, + StatusBarTapResultStop, +} NS_SWIFT_NAME(StatusBarTapResult); + +NS_SWIFT_UI_ACTOR +@protocol StatusBarTapHandling + +- (StatusBarTapResult)handleStatusBarTappedAtXPosition:(CGFloat)xPosition NS_SWIFT_NAME(handleStatusBarTapped(at:)); + +@end + +@interface UIStatusBarManager (TuskerSwizzling) + +@property (nonatomic, readonly) UIWindowScene * _Nullable windowScene; + +- (void)handleTapAction:(id)sender; + +@end +#endif + +NS_ASSUME_NONNULL_END diff --git a/Tusker/Swizzler.m b/Tusker/Swizzler.m new file mode 100644 index 0000000000..f21e1e2b4c --- /dev/null +++ b/Tusker/Swizzler.m @@ -0,0 +1,37 @@ +// +// Swizzler.m +// Tusker +// +// Created by Shadowfacts on 3/3/25. +// Copyright © 2025 Shadowfacts. All rights reserved. +// + +#import "Swizzler.h" +#import + +@implementation Swizzler + ++ (BOOL)swizzleStatusBarManagerWithExceptionHandler:(void (^)(NSException *))captureException { + __block IMP original; + IMP new = imp_implementationWithBlock(^void (UIStatusBarManager *self, id sender) { + if ([self.windowScene.delegate conformsToProtocol:@protocol(StatusBarTapHandling)]) { + CGFloat xPosition; + @try { + xPosition = [(NSNumber *)[sender valueForKey:@"xPosition"] doubleValue]; + } @catch (NSException *exception) { + captureException(exception); + return; + } + id delegate = (id)self.windowScene.delegate; + StatusBarTapResult result = [delegate handleStatusBarTappedAtXPosition:xPosition]; + if (result == StatusBarTapResultStop) { + return; + } + } + ((void (*)(UIStatusBarManager *, SEL, id))original)(self, @selector(handleTapAction:), sender); + }); + original = class_replaceMethod(UIStatusBarManager.class, @selector(handleTapAction:), new, "v@:@"); + return original != nil; +} + +@end diff --git a/Tusker/Tusker-Bridging-Header.h b/Tusker/Tusker-Bridging-Header.h index 8eab486e5f..fc97e6d53e 100644 --- a/Tusker/Tusker-Bridging-Header.h +++ b/Tusker/Tusker-Bridging-Header.h @@ -4,16 +4,7 @@ #import #import - -NS_INLINE NSException * _Nullable catchNSException(void(^_Nonnull tryBlock)(void)) { - @try { - tryBlock(); - } - @catch (NSException *exception) { - return exception; - } - return nil; -} +#import "Swizzler.h" // Define this private method so we can override it from MultiColumnCollectionViewLayout. @interface UICollectionViewLayout (Tusker_Hacks)