Remove catchNSException and instead swizzle things in Objective C

Apparently any Swift stack frames between throwing/catching an NSException
can break things.
This commit is contained in:
Shadowfacts 2025-03-03 12:40:41 -05:00
parent 57e21176f0
commit 84ed9e92ee
31 changed files with 175 additions and 137 deletions

View File

@ -113,7 +113,6 @@
D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D63CC701290EC0B8000E19DE /* Sentry */; }; D63CC702290EC0B8000E19DE /* Sentry in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D63CC701290EC0B8000E19DE /* Sentry */; };
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; }; D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */; };
D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63CC70F2911F1E4000E19DE /* UIScrollView+Top.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 */; }; D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D63D8DF32850FE7A008D95E1 /* ViewTags.swift */; };
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; }; D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */ = {isa = PBXBuildFile; fileRef = D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */; };
D6412B0324AFF6A600F5412E /* TabBarScrollableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */; }; 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 */; }; D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B4B8A297879E900DABDFB /* AccountFollowsViewController.swift */; };
D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; }; D65C6BF525478A9C00A6E89C /* BackgroundableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65C6BF425478A9C00A6E89C /* BackgroundableViewController.swift */; };
D65F613423AEAB6600F3CFD3 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65F613323AEAB6600F3CFD3 /* OnboardingTests.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 */; }; D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */; };
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.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 */; }; 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 = "<group>"; }; D63CC703290EC472000E19DE /* Dist.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dist.xcconfig; sourceTree = "<group>"; };
D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerSceneDelegate.swift; sourceTree = "<group>"; }; D63CC70B2910AADB000E19DE /* TuskerSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerSceneDelegate.swift; sourceTree = "<group>"; };
D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = "<group>"; }; D63CC70F2911F1E4000E19DE /* UIScrollView+Top.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Top.swift"; sourceTree = "<group>"; };
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarTappableViewController.swift; sourceTree = "<group>"; };
D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; }; D63D8DF32850FE7A008D95E1 /* ViewTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewTags.swift; sourceTree = "<group>"; };
D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; }; D640D76822BAF5E6004FBE69 /* DomainBlocks.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DomainBlocks.plist; sourceTree = "<group>"; };
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; }; D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarScrollableViewController.swift; sourceTree = "<group>"; };
@ -595,6 +594,8 @@
D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; }; D65F613323AEAB6600F3CFD3 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; };
D65F613523AFD65900F3CFD3 /* Ambassador.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Ambassador.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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 = "<group>"; };
D6606A0F2D761BA3004BBEF4 /* Swizzler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Swizzler.m; sourceTree = "<group>"; };
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; }; D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
@ -1542,7 +1543,6 @@
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */, D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */,
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */, D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */,
D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */, D691771429A6FCAB0054D7EF /* StateRestorableViewController.swift */,
D63CC7112911F57C000E19DE /* StatusBarTappableViewController.swift */,
D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */, D6412B0224AFF6A600F5412E /* TabBarScrollableViewController.swift */,
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */, D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */,
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */, D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */,
@ -1606,6 +1606,8 @@
D60088F02980D938005B4D00 /* Tusker.storekit */, D60088F02980D938005B4D00 /* Tusker.storekit */,
D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */, D691296D2BA75ACF005C58ED /* PrivacyInfo.xcprivacy */,
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */, D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */,
D6606A0E2D761BA3004BBEF4 /* Swizzler.h */,
D6606A0F2D761BA3004BBEF4 /* Swizzler.m */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */, D6EEDE922C3CF21800E10E51 /* AudioSessionCoordinator.swift */,
D6D79F582A13293200AB2315 /* BackgroundManager.swift */, D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
@ -2157,7 +2159,6 @@
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */, 0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */, D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */,
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */, D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */,
D63CC7122911F57C000E19DE /* StatusBarTappableViewController.swift in Sources */,
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */, D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */,
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */, D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */, D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */,
@ -2215,6 +2216,7 @@
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */, D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */, D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */, D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
D6606A102D761BA3004BBEF4 /* Swizzler.m in Sources */,
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */, D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
D68245122BCA1F4000AFB38B /* NotificationLoadingViewController.swift in Sources */, D68245122BCA1F4000AFB38B /* NotificationLoadingViewController.swift in Sources */,

View File

@ -30,7 +30,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
configureSentry() configureSentry()
#endif #endif
#if !os(visionOS) #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() swizzlePresentationController()
#endif #endif
@ -202,35 +207,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
} }
#if !os(visionOS) #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) @available(iOS, obsoleted: 17.0)
private func swizzlePresentationController() { private func swizzlePresentationController() {
guard #unavailable(iOS 17.0) else { guard #unavailable(iOS 17.0) else {

View File

@ -130,4 +130,8 @@ class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDel
@objc private func themePrefChanged() { @objc private func themePrefChanged() {
applyAppearancePreferences() applyAppearancePreferences()
} }
func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
doHandleStatusBarTapped(at: xPosition)
}
} }

View File

@ -113,6 +113,10 @@ class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDeleg
applyAppearancePreferences() applyAppearancePreferences()
} }
func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
doHandleStatusBarTapped(at: xPosition)
}
} }
extension ComposeSceneDelegate: ComposeHostingControllerDelegate { extension ComposeSceneDelegate: ComposeHostingControllerDelegate {

View File

@ -310,6 +310,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
preferencesVC?.navigationState.showNotificationPreferences = true preferencesVC?.navigationState.showNotificationPreferences = true
} }
func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
doHandleStatusBarTapped(at: xPosition)
}
} }
extension MainSceneDelegate: OnboardingViewControllerDelegate { extension MainSceneDelegate: OnboardingViewControllerDelegate {

View File

@ -12,21 +12,16 @@ import Sentry
#endif #endif
@MainActor @MainActor
protocol TuskerSceneDelegate: UISceneDelegate { protocol TuskerSceneDelegate: UISceneDelegate, StatusBarTapHandling {
var window: UIWindow? { get } var window: UIWindow? { get }
var rootViewController: TuskerRootViewController? { get } var rootViewController: TuskerRootViewController? { get }
} }
enum StatusBarTapActionResult {
case `continue`
case stop
}
extension TuskerSceneDelegate { extension TuskerSceneDelegate {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func doHandleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
if let rootViewController { if let rootViewController {
let converted = rootViewController.view.convert(CGPoint(x: xPosition, y: 0), from: nil) 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 return .continue
} }
@ -41,14 +36,9 @@ extension TuskerSceneDelegate {
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {
window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode window.traitOverrides.pureBlackDarkMode = Preferences.shared.pureBlackDarkMode
} else { } else {
let exception = catchNSException { let key = ["Controller", "Presentation", "root", "_"].reversed().joined()
let key = ["Controller", "Presentation", "root", "_"].reversed().joined() if let rootPresentationController = window.value(forKey: key) as? UIPresentationController {
if let rootPresentationController = window.value(forKey: key) as? UIPresentationController { rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
rootPresentationController.overrideTraitCollection = UITraitCollection(pureBlackDarkMode: Preferences.shared.pureBlackDarkMode)
}
}
if let exception {
SentrySDK.capture(exception: exception)
} }
} }
#endif #endif

View File

@ -297,8 +297,8 @@ extension AccountFollowsListViewController: MenuActionProvider {
extension AccountFollowsListViewController: ToastableViewController { extension AccountFollowsListViewController: ToastableViewController {
} }
extension AccountFollowsListViewController: StatusBarTappableViewController { extension AccountFollowsListViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -132,8 +132,8 @@ extension AccountListViewController: TuskerNavigationDelegate {
extension AccountListViewController: MenuActionProvider { extension AccountListViewController: MenuActionProvider {
} }
extension AccountListViewController: StatusBarTappableViewController { extension AccountListViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -462,8 +462,8 @@ extension ConversationCollectionViewController: TabBarScrollableViewController {
} }
} }
extension ConversationCollectionViewController: StatusBarTappableViewController { extension ConversationCollectionViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -430,10 +430,10 @@ extension ConversationViewController: ToastableViewController {
} }
} }
extension ConversationViewController: StatusBarTappableViewController { extension ConversationViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
if case .displaying(let vc) = state { if case .displaying(let vc) = state {
return vc.handleStatusBarTapped(xPosition: xPosition) return vc.handleStatusBarTapped(at: xPosition)
} else { } else {
return .continue return .continue
} }

View File

@ -267,8 +267,8 @@ extension TrendingStatusesViewController: StatusCollectionViewCellDelegate {
} }
} }
extension TrendingStatusesViewController: StatusBarTappableViewController { extension TrendingStatusesViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -446,8 +446,8 @@ extension LocalPredicateStatusesViewController: TabBarScrollableViewController {
} }
} }
extension LocalPredicateStatusesViewController: StatusBarTappableViewController { extension LocalPredicateStatusesViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -184,12 +184,12 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
return root.presentPreferences(completion: completion) return root.presentPreferences(completion: completion)
} }
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
loadViewIfNeeded() loadViewIfNeeded()
if root.isFastAccountSwitcherActive { if root.isFastAccountSwitcherActive {
return .stop return .stop
} else { } else {
return root.handleStatusBarTapped(xPosition: xPosition) return root.handleStatusBarTapped(at: xPosition)
} }
} }
} }

View File

@ -198,12 +198,12 @@ extension BaseMainTabBarViewController: BackgroundableViewController {
} }
} }
extension BaseMainTabBarViewController: StatusBarTappableViewController { extension BaseMainTabBarViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
guard presentedViewController == nil, guard presentedViewController == nil,
let vc = selectedViewController as? StatusBarTappableViewController else { let vc = selectedViewController as? StatusBarTapHandling else {
return .continue return .continue
} }
return vc.handleStatusBarTapped(xPosition: xPosition) return vc.handleStatusBarTapped(at: xPosition)
} }
} }

View File

@ -12,6 +12,13 @@ import UIKit
import Duckable import Duckable
import ComposeUI 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, *) @available(iOS 16.0, *)
extension DuckableContainerViewController: AccountSwitchableViewController { extension DuckableContainerViewController: AccountSwitchableViewController {
func stateRestorationActivity() -> NSUserActivity? { func stateRestorationActivity() -> NSUserActivity? {
@ -48,10 +55,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController {
(child as? TuskerRootViewController)?.presentPreferences(completion: completion) (child as? TuskerRootViewController)?.presentPreferences(completion: completion)
} }
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
(child as? TuskerRootViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue
}
var isFastAccountSwitcherActive: Bool { var isFastAccountSwitcherActive: Bool {
(child as? AccountSwitchableViewController)?.isFastAccountSwitcherActive ?? false (child as? AccountSwitchableViewController)?.isFastAccountSwitcherActive ?? false
} }

View File

@ -648,17 +648,17 @@ extension MainSplitViewController: TuskerRootViewController {
return vc return vc
} }
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
guard presentedViewController == nil else { guard presentedViewController == nil else {
return .continue return .continue
} }
if traitCollection.horizontalSizeClass == .compact { if traitCollection.horizontalSizeClass == .compact {
return tabBarViewController.handleStatusBarTapped(xPosition: xPosition) return tabBarViewController.handleStatusBarTapped(at: xPosition)
} else { } else {
let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view) let pointInSecondary = secondaryNavController.view.convert(CGPoint(x: xPosition, y: 0), from: view)
if secondaryNavController.view.bounds.contains(pointInSecondary), if secondaryNavController.view.bounds.contains(pointInSecondary),
let statusBarTappable = secondaryNavController as? StatusBarTappableViewController { let statusBarTappable = secondaryNavController as? StatusBarTapHandling {
return statusBarTappable.handleStatusBarTapped(xPosition: pointInSecondary.x) return statusBarTappable.handleStatusBarTapped(at: pointInSecondary.x)
} else { } else {
return .continue return .continue
} }

View File

@ -10,7 +10,7 @@ import UIKit
import ComposeUI import ComposeUI
@MainActor @MainActor
protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTapHandling {
func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?)
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?)
func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationDelegate() -> TuskerNavigationDelegate?

View File

@ -136,8 +136,8 @@ extension FollowRequestNotificationViewController: TuskerNavigationDelegate {
extension FollowRequestNotificationViewController: MenuActionProvider { extension FollowRequestNotificationViewController: MenuActionProvider {
} }
extension FollowRequestNotificationViewController: StatusBarTappableViewController { extension FollowRequestNotificationViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -803,8 +803,8 @@ extension NotificationsCollectionViewController: TabBarScrollableViewController
} }
} }
extension NotificationsCollectionViewController: StatusBarTappableViewController { extension NotificationsCollectionViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -690,8 +690,8 @@ extension ProfileStatusesViewController: TabBarScrollableViewController {
} }
} }
extension ProfileStatusesViewController: StatusBarTappableViewController { extension ProfileStatusesViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -390,10 +390,10 @@ extension ProfileViewController: TabBarScrollableViewController {
} }
} }
extension ProfileViewController: StatusBarTappableViewController { extension ProfileViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
guard isViewLoaded else { return .stop } guard isViewLoaded else { return .stop }
return currentViewController.handleStatusBarTapped(xPosition: xPosition) return currentViewController.handleStatusBarTapped(at: xPosition)
} }
} }

View File

@ -425,8 +425,8 @@ extension StatusActionAccountListCollectionViewController: StatusCollectionViewC
} }
} }
extension StatusActionAccountListCollectionViewController: StatusBarTappableViewController { extension StatusActionAccountListCollectionViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -1437,8 +1437,8 @@ extension TimelineViewController: TabBarScrollableViewController {
} }
} }
extension TimelineViewController: StatusBarTappableViewController { extension TimelineViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
collectionView.scrollToTop() collectionView.scrollToTop()
return .stop return .stop
} }

View File

@ -160,8 +160,8 @@ extension AdaptableNavigationController: BackgroundableViewController {
} }
@available(iOS 17.0, *) @available(iOS 17.0, *)
extension AdaptableNavigationController: StatusBarTappableViewController { extension AdaptableNavigationController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
(topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue (topViewController as? StatusBarTapHandling)?.handleStatusBarTapped(at: xPosition) ?? .continue
} }
} }

View File

@ -259,10 +259,10 @@ extension EnhancedNavigationViewController: BackgroundableViewController {
} }
} }
extension EnhancedNavigationViewController: StatusBarTappableViewController { extension EnhancedNavigationViewController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
if let topVC = topViewController as? StatusBarTappableViewController { if let topVC = topViewController as? StatusBarTapHandling {
return topVC.handleStatusBarTapped(xPosition: xPosition) return topVC.handleStatusBarTapped(at: xPosition)
} }
return .continue return .continue
} }

View File

@ -12,7 +12,7 @@ protocol SegmentedPageViewControllerPage: Hashable {
var segmentedControlTitle: String { get } var segmentedControlTitle: String { get }
} }
class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIViewController, UIPageViewControllerDelegate, TabbedPageViewController { class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIViewController, UIPageViewControllerDelegate, TabbedPageViewController, StatusBarTapHandling {
private(set) var pages: [Page]! private(set) var pages: [Page]!
private let pageProvider: (Page) -> UIViewController private let pageProvider: (Page) -> UIViewController
@ -178,6 +178,14 @@ class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIView
guard currentIndex > 0 else { return } guard currentIndex > 0 else { return }
selectPage(pages[currentIndex - 1], animated: true) 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 { 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 { extension SegmentedPageViewController: NestedResponderProvider {
var innerResponder: UIResponder? { var innerResponder: UIResponder? {
currentViewController currentViewController

View File

@ -270,18 +270,18 @@ extension SplitNavigationController: TabBarScrollableViewController {
} }
} }
extension SplitNavigationController: StatusBarTappableViewController { extension SplitNavigationController: StatusBarTapHandling {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { func handleStatusBarTapped(at xPosition: CGFloat) -> StatusBarTapResult {
let vcs = viewControllers let vcs = viewControllers
if !canShowSecondaryNav || vcs.count < 2 { if !canShowSecondaryNav || vcs.count < 2 {
return (vcs.last! as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue return (vcs.last! as? StatusBarTapHandling)?.handleStatusBarTapped(at: xPosition) ?? .continue
} else { } else {
let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view) let positionInRoot = rootNav.view.convert(CGPoint(x: xPosition, y: 0), from: view)
let positionInSecondary = secondaryNav.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) { 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) { } 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 return .continue

View File

@ -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
}

42
Tusker/Swizzler.h Normal file
View File

@ -0,0 +1,42 @@
//
// Swizzler.h
// Tusker
//
// Created by Shadowfacts on 3/3/25.
// Copyright © 2025 Shadowfacts. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <TargetConditionals.h>
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 <NSObject>
- (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

37
Tusker/Swizzler.m Normal file
View File

@ -0,0 +1,37 @@
//
// Swizzler.m
// Tusker
//
// Created by Shadowfacts on 3/3/25.
// Copyright © 2025 Shadowfacts. All rights reserved.
//
#import "Swizzler.h"
#import <objc/runtime.h>
@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<StatusBarTapHandling> delegate = (id<StatusBarTapHandling>)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

View File

@ -4,16 +4,7 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import "Swizzler.h"
NS_INLINE NSException * _Nullable catchNSException(void(^_Nonnull tryBlock)(void)) {
@try {
tryBlock();
}
@catch (NSException *exception) {
return exception;
}
return nil;
}
// Define this private method so we can override it from MultiColumnCollectionViewLayout. // Define this private method so we can override it from MultiColumnCollectionViewLayout.
@interface UICollectionViewLayout (Tusker_Hacks) @interface UICollectionViewLayout (Tusker_Hacks)