Instantly share code, notes, and snippets.
Last active
December 15, 2023 03:40
-
Save maximkrouk/f248a0d7013b970e3d7c1161078b3727 to your computer and use it in GitHub Desktop.
UINavigationController.popPublisher implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import CocoaAliases // https://github.com/capturecontext/cocoa-aliases | |
import FoundationExtensions // https://github.com/capturecontext/swift-foundation-extensions | |
// MARK: - Swizzling | |
// Swizzle methods that may pop some viewControllers | |
// with tracking versions which forward popped controllers | |
// to UINavigationController.popSubject | |
// Swift swizzling causes infinite recursion for objc methods | |
// | |
// Forum: | |
// https://forums.swift.org/t/dynamicreplacement-causes-infinite-recursion/52768 | |
// | |
// Swift issues: | |
// https://github.com/apple/swift/issues/62214 | |
// https://github.com/apple/swift/issues/53916 | |
// | |
// Have to use objc swizzling | |
// | |
//extension UINavigationController { | |
// @_dynamicReplacement(for: popViewController(animated:)) | |
// public func _trackedPopViewController( | |
// animated: Bool | |
// ) -> CocoaViewController? { | |
// let controller = popViewController(animated: animated) | |
// controller.map { handlePop(of: [$0]) } | |
// return controller | |
// } | |
// | |
// @_dynamicReplacement(for: popToRootViewController(animated:)) | |
// public func _trackedPopToRootViewController( | |
// animated: Bool | |
// ) -> [CocoaViewController]? { | |
// let controllers = popToRootViewController(animated: animated) | |
// controllers.map(handlePop) | |
// return controllers | |
// } | |
// | |
// @_dynamicReplacement(for: popToViewController(_:animated:)) | |
// public func _trackedPopToViewController( | |
// _ controller: CocoaViewController, | |
// animated: Bool | |
// ) -> [CocoaViewController]? { | |
// let controllers = popToViewController(controller, animated: animated) | |
// controllers.map(handlePop) | |
// return controllers | |
// } | |
// | |
// @_dynamicReplacement(for: setViewControllers(_:animated:)) | |
// public func _trackedSetViewControllers( | |
// _ controllers: [CocoaViewController], | |
// animated: Bool | |
// ) { | |
// let poppedControllers = viewControllers.filter { oldController in | |
// !controllers.contains { $0 === oldController } | |
// } | |
// | |
// setViewControllers(controllers, animated: animated) | |
// handlePop(of: poppedControllers) | |
// } | |
//} | |
extension UINavigationController { | |
// Runs once in app lifetime | |
// Repeated calls do nothing | |
public static let enablePopPublisher: () -> Void = { | |
UINavigationController._swizzle( | |
#selector(popViewController(animated:)), | |
with: #selector(__swizzledPopViewController) | |
) | |
UINavigationController._swizzle( | |
#selector(popToViewController(_:animated:)), | |
with: #selector(__swizzledPopToViewController) | |
) | |
UINavigationController._swizzle( | |
#selector(popToRootViewController(animated:)), | |
with: #selector(__swizzledPopToRootViewController) | |
) | |
UINavigationController._swizzle( | |
#selector(setViewControllers(_:animated:)), | |
with: #selector(__swizzledSetViewControllers) | |
) | |
return {} | |
}() | |
// Swizzle automatically when the first | |
// navigationController loads it's view | |
open override func loadView() { | |
UINavigationController.enablePopPublisher() | |
super.viewDidLoad() | |
} | |
@objc dynamic func __swizzledPopViewController( | |
animated: Bool | |
) -> CocoaViewController? { | |
let controller = __swizzledPopViewController(animated: animated) | |
controller.map { handlePop(of: [$0]) } | |
return controller | |
} | |
@objc dynamic func __swizzledPopToRootViewController( | |
animated: Bool | |
) -> [CocoaViewController]? { | |
let controllers = __swizzledPopToRootViewController(animated: animated) | |
controllers.map(handlePop) | |
return controllers | |
} | |
@objc dynamic func __swizzledPopToViewController( | |
_ controller: CocoaViewController, | |
animated: Bool | |
) -> [CocoaViewController]? { | |
let controllers = __swizzledPopToViewController(controller, animated: animated) | |
controllers.map(handlePop) | |
return controllers | |
} | |
@objc dynamic func __swizzledSetViewControllers( | |
_ controllers: [CocoaViewController], | |
animated: Bool | |
) { | |
let poppedControllers = viewControllers.filter { oldController in | |
!controllers.contains { $0 === oldController } | |
} | |
__swizzledSetViewControllers(controllers, animated: animated) | |
handlePop(of: poppedControllers) | |
} | |
} | |
// MARK: Objc swizzling helper | |
#warning("Move to FoundationExtensions") | |
fileprivate protocol NSObjectSwizzling: NSObjectProtocol {} | |
extension NSObjectSwizzling { | |
static func _swizzle( | |
_ originalSelector: Selector, | |
with swizzledSelector: Selector | |
) { | |
let originalMethod = class_getInstanceMethod( | |
Self.self, | |
originalSelector | |
) | |
let swizzledMethod = class_getInstanceMethod( | |
Self.self, | |
swizzledSelector | |
) | |
guard | |
let originalMethod, | |
let swizzledMethod | |
else { return } | |
method_exchangeImplementations(originalMethod, swizzledMethod) | |
} | |
} | |
extension NSObject: NSObjectSwizzling {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Back to index