Skip to content

Commit d319d7f

Browse files
authored
Fix no animation when popping multiple items off the path (#44)
Fixes #41 by swizzling `popToViewController(_:animated:)` and forcing `animated: true`.
1 parent a3e4687 commit d319d7f

File tree

5 files changed

+131
-27
lines changed

5 files changed

+131
-27
lines changed

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ package.targets += [
6262

6363
.target(name: "NavigationTransitions", dependencies: [
6464
"NavigationTransition",
65+
"RuntimeAssociation",
66+
"RuntimeSwizzling",
6567
]),
6668

69+
.target(name: "RuntimeAssociation"),
70+
.target(name: "RuntimeSwizzling"),
71+
6772
.target(name: "TestUtils", dependencies: [
6873
CustomDump,
6974
"NavigationTransitions",

Sources/NavigationTransitions/NavigationTransition+UIKit.swift

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
@_spi(package) import NavigationTransition
2+
import RuntimeAssociation
3+
import RuntimeSwizzling
24
import UIKit
35

46
extension AnyNavigationTransition {
@@ -115,21 +117,16 @@ extension RandomAccessCollection where Index == Int {
115117
}
116118
}
117119

118-
extension UINavigationController {
119-
private static var defaultDelegateKey = "defaultDelegate"
120-
private static var customDelegateKey = "customDelegate"
121-
120+
extension UINavigationController: RuntimeAssociation {
122121
private var defaultDelegate: UINavigationControllerDelegate! {
123-
get { objc_getAssociatedObject(self, &Self.defaultDelegateKey) as? UINavigationControllerDelegate }
124-
set { objc_setAssociatedObject(self, &Self.defaultDelegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
122+
get { self[] }
123+
set { self[] = newValue }
125124
}
126125

127126
var customDelegate: NavigationTransitionDelegate! {
128-
get {
129-
objc_getAssociatedObject(self, &Self.customDelegateKey) as? NavigationTransitionDelegate
130-
}
127+
get { self[] }
131128
set {
132-
objc_setAssociatedObject(self, &Self.customDelegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
129+
self[] = newValue
133130
delegate = newValue
134131
}
135132
}
@@ -138,6 +135,13 @@ extension UINavigationController {
138135
_ transition: AnyNavigationTransition,
139136
interactivity: AnyNavigationTransition.Interactivity = .default
140137
) {
138+
// fix for https://stackoverflow.com/q/73753796/1922543
139+
swizzle(
140+
UINavigationController.self,
141+
#selector(UINavigationController.popToViewController),
142+
#selector(UINavigationController.popToViewController_forceAnimated)
143+
)
144+
141145
if defaultDelegate == nil {
142146
defaultDelegate = delegate
143147
}
@@ -201,42 +205,41 @@ extension UINavigationController {
201205
}
202206
}
203207

208+
extension UINavigationController {
209+
@objc func popToViewController_forceAnimated(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
210+
// TODO: `if #unavailable(iOS 17?...)` when fixed by Apple
211+
popToViewController_forceAnimated(viewController, animated: true)
212+
}
213+
}
214+
204215
@available(tvOS, unavailable)
205216
extension UINavigationController {
206217
var defaultEdgePanRecognizer: UIScreenEdgePanGestureRecognizer! {
207218
interactivePopGestureRecognizer as? UIScreenEdgePanGestureRecognizer
208219
}
209220

210-
private static var defaultInteractivePanGestureRecognizer = "defaultInteractivePanGestureRecognizer"
211-
private static var interactiveEdgePanGestureRecognizer = "interactiveEdgePanGestureRecognizer"
212-
private static var interactivePanGestureRecognizer = "interactivePanGestureRecognizer"
213-
214221
var defaultPanRecognizer: UIPanGestureRecognizer! {
215-
get { objc_getAssociatedObject(self, &Self.defaultInteractivePanGestureRecognizer) as? UIPanGestureRecognizer }
216-
set { objc_setAssociatedObject(self, &Self.defaultInteractivePanGestureRecognizer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
222+
get { self[] }
223+
set { self[] = newValue }
217224
}
218225

219226
var edgePanRecognizer: UIScreenEdgePanGestureRecognizer! {
220-
get { objc_getAssociatedObject(self, &Self.interactiveEdgePanGestureRecognizer) as? UIScreenEdgePanGestureRecognizer }
221-
set { objc_setAssociatedObject(self, &Self.interactiveEdgePanGestureRecognizer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
227+
get { self[] }
228+
set { self[] = newValue }
222229
}
223230

224231
var panRecognizer: UIPanGestureRecognizer! {
225-
get { objc_getAssociatedObject(self, &Self.interactivePanGestureRecognizer) as? UIPanGestureRecognizer }
226-
set { objc_setAssociatedObject(self, &Self.interactivePanGestureRecognizer, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
232+
get { self[] }
233+
set { self[] = newValue }
227234
}
228235
}
229236

230237
@available(tvOS, unavailable)
231-
extension UIGestureRecognizer {
232-
private static var strongDelegateKey = "strongDelegateKey"
233-
238+
extension UIGestureRecognizer: RuntimeAssociation {
234239
var strongDelegate: UIGestureRecognizerDelegate? {
235-
get {
236-
objc_getAssociatedObject(self, &Self.strongDelegateKey) as? UIGestureRecognizerDelegate
237-
}
240+
get { self[] }
238241
set {
239-
objc_setAssociatedObject(self, &Self.strongDelegateKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
242+
self[] = newValue
240243
delegate = newValue
241244
}
242245
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import ObjectiveC
2+
3+
public protocol RuntimeAssociation: AnyObject {
4+
subscript<T>(forKey key: String, policy: RuntimeAssociationPolicy) -> T? { get set }
5+
}
6+
7+
extension RuntimeAssociation {
8+
public subscript<T>(forKey key: String = #function, policy: RuntimeAssociationPolicy = .retain(.nonatomic)) -> T? {
9+
get {
10+
let key = unsafeBitCast(Selector(key), to: UnsafeRawPointer.self)
11+
return objc_getAssociatedObject(self, key) as? T
12+
}
13+
set {
14+
let key = unsafeBitCast(Selector(key), to: UnsafeRawPointer.self)
15+
objc_setAssociatedObject(self, key, newValue, .init(policy))
16+
}
17+
}
18+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import ObjectiveC
2+
3+
public enum RuntimeAssociationPolicy {
4+
public enum Atomicity {
5+
case atomic
6+
case nonatomic
7+
}
8+
9+
case assign
10+
case copy(Atomicity)
11+
case retain(Atomicity)
12+
}
13+
14+
extension objc_AssociationPolicy {
15+
init(_ policy: RuntimeAssociationPolicy) {
16+
switch policy {
17+
case .assign:
18+
self = .OBJC_ASSOCIATION_ASSIGN
19+
case .copy(.atomic):
20+
self = .OBJC_ASSOCIATION_COPY
21+
case .copy(.nonatomic):
22+
self = .OBJC_ASSOCIATION_COPY_NONATOMIC
23+
case .retain(.atomic):
24+
self = .OBJC_ASSOCIATION_RETAIN
25+
case .retain(.nonatomic):
26+
self = .OBJC_ASSOCIATION_RETAIN_NONATOMIC
27+
}
28+
}
29+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import ObjectiveC
2+
3+
public var swizzleLogs = false
4+
5+
public func swizzle(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) {
6+
guard !swizzlingHistory.contains(type, original, swizzled) else {
7+
return
8+
}
9+
10+
swizzlingHistory.add(type, original, swizzled)
11+
12+
guard let originalMethod = class_getInstanceMethod(type, original) else {
13+
assertionFailure("[Swizzling] Instance method \(type).\(original) not found.")
14+
return
15+
}
16+
17+
guard let swizzledMethod = class_getInstanceMethod(type, swizzled) else {
18+
assertionFailure("[Swizzling] Instance method \(type).\(swizzled) not found.")
19+
return
20+
}
21+
22+
if swizzleLogs {
23+
print("[Swizzling] [\(type) \(original) <~> \(swizzled)]")
24+
}
25+
26+
method_exchangeImplementations(originalMethod, swizzledMethod)
27+
}
28+
29+
fileprivate struct SwizzlingHistory {
30+
private var map: [Int: Void] = [:]
31+
32+
func contains(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) -> Bool {
33+
map[hash(type, original, swizzled)] != nil
34+
}
35+
36+
mutating func add(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) {
37+
map[hash(type, original, swizzled)] = ()
38+
}
39+
40+
private func hash(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) -> Int {
41+
var hasher = Hasher()
42+
hasher.combine(ObjectIdentifier(type))
43+
hasher.combine(original)
44+
hasher.combine(swizzled)
45+
return hasher.finalize()
46+
}
47+
}
48+
49+
fileprivate var swizzlingHistory = SwizzlingHistory()

0 commit comments

Comments
 (0)