Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Last active December 15, 2023 03:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save maximkrouk/f248a0d7013b970e3d7c1161078b3727 to your computer and use it in GitHub Desktop.
Save maximkrouk/f248a0d7013b970e3d7c1161078b3727 to your computer and use it in GitHub Desktop.
UINavigationController.popPublisher implementation
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 {}
@maximkrouk
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment