diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea2a64e3..5d455f14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,13 +26,17 @@ jobs: - mac-catalyst - tvOS swift: - - "5.10" - "6.0" - "6.1" steps: - name: Git Checkout uses: actions/checkout@v4 + - name: Disable Macro Fingerprint Validation + run: | + defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES + defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES + - name: Test Library uses: mxcl/xcodebuild@v3 with: diff --git a/.swift-version b/.swift-version index f9ce5a96..e0ea36fe 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -5.10 +6.0 diff --git a/Package.resolved b/Package.resolved index ac5c811c..8f339f19 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "6055238988eaf036fed98cc74283a630a84b8b7c28e6fa1948e8e7d489cf9dbe", + "originHash" : "296e08fe1d329d42bb2a4aca5559e7dbe53ae453d7d1be1179f159a52bb9fb2b", "pins" : [ + { + "identity" : "objc-runtime-tools", + "kind" : "remoteSourceControl", + "location" : "https://github.com/davdroman/objc-runtime-tools", + "state" : { + "revision" : "c05d245cbb8d289a6bbc184fd2d1605b70ec65f3", + "version" : "0.1.0" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", @@ -10,6 +19,24 @@ "version" : "1.3.3" } }, + { + "identity" : "swift-once-macro", + "kind" : "remoteSourceControl", + "location" : "https://github.com/davdroman/swift-once-macro", + "state" : { + "revision" : "2bfa91668d16723902cdc558c1a736e8787c91c6", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" + } + }, { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 5c408720..99666ded 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.10 +// swift-tools-version: 6.0 import PackageDescription @@ -37,33 +37,31 @@ let package = Package( ]), .target(name: "UIKitNavigationTransitions", dependencies: [ + .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), "NavigationTransition", - "RuntimeAssociation", - "RuntimeSwizzling", + .product(name: "ObjCRuntimeTools", package: "objc-runtime-tools"), + .product(name: "Once", package: "swift-once-macro"), ]), .target(name: "SwiftUINavigationTransitions", dependencies: [ - "NavigationTransition", - "RuntimeAssociation", - "RuntimeSwizzling", "UIKitNavigationTransitions", .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), ]), - .target(name: "RuntimeAssociation"), - .target(name: "RuntimeSwizzling"), - .target(name: "TestUtils", dependencies: [ .product(name: "CustomDump", package: "swift-custom-dump"), .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), "SwiftUINavigationTransitions", ]), - ] + ], + swiftLanguageModes: [.v5] ) // MARK: Dependencies package.dependencies = [ + .package(url: "https://github.com/davdroman/objc-runtime-tools", from: "0.1.0"), + .package(url: "https://github.com/davdroman/swift-once-macro", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), // dev .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), .package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"), @@ -72,6 +70,7 @@ package.dependencies = [ for target in package.targets { target.swiftSettings = target.swiftSettings ?? [] target.swiftSettings? += [ - .enableExperimentalFeature("AccessLevelOnImport"), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), ] } diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift deleted file mode 100644 index a9db53fc..00000000 --- a/Package@swift-6.0.swift +++ /dev/null @@ -1,79 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "swiftui-navigation-transitions", - platforms: [ - .iOS(.v13), - .macCatalyst(.v13), - .tvOS(.v13), - ], - products: [ - .library(name: "SwiftUINavigationTransitions", targets: ["SwiftUINavigationTransitions"]), - .library(name: "UIKitNavigationTransitions", targets: ["UIKitNavigationTransitions"]), - ], - targets: [ - .target(name: "Animation"), - - .target(name: "Animator"), - .testTarget(name: "AnimatorTests", dependencies: [ - "Animator", - "TestUtils", - ]), - - .target(name: "AtomicTransition", dependencies: [ - "Animator", - ]), - .testTarget(name: "AtomicTransitionTests", dependencies: [ - "AtomicTransition", - "TestUtils", - ]), - - .target(name: "NavigationTransition", dependencies: [ - "Animation", - "AtomicTransition", - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - ]), - - .target(name: "UIKitNavigationTransitions", dependencies: [ - "NavigationTransition", - "RuntimeAssociation", - "RuntimeSwizzling", - ]), - - .target(name: "SwiftUINavigationTransitions", dependencies: [ - "NavigationTransition", - "RuntimeAssociation", - "RuntimeSwizzling", - "UIKitNavigationTransitions", - .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), - ]), - - .target(name: "RuntimeAssociation"), - .target(name: "RuntimeSwizzling"), - - .target(name: "TestUtils", dependencies: [ - .product(name: "CustomDump", package: "swift-custom-dump"), - .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), - "SwiftUINavigationTransitions", - ]), - ], - swiftLanguageModes: [.v5] -) - -// MARK: Dependencies - -package.dependencies = [ - .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"), // dev - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), - .package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"), -] - -for target in package.targets { - target.swiftSettings = target.swiftSettings ?? [] - target.swiftSettings? += [ - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault"), - ] -} diff --git a/Sources/Animation/InterpolatingSpring.swift b/Sources/Animation/InterpolatingSpring.swift index b41c5b4f..1266494b 100644 --- a/Sources/Animation/InterpolatingSpring.swift +++ b/Sources/Animation/InterpolatingSpring.swift @@ -1,4 +1,4 @@ -internal import UIKit // TODO: remove internal from all imports when Swift 5.10 is dropped +import UIKit extension Animation { public static func interpolatingSpring( diff --git a/Sources/Animation/TimingCurve.swift b/Sources/Animation/TimingCurve.swift index eb47f683..019adf85 100644 --- a/Sources/Animation/TimingCurve.swift +++ b/Sources/Animation/TimingCurve.swift @@ -1,4 +1,4 @@ -internal import UIKit // TODO: remove internal from all imports when Swift 5.10 is dropped +import UIKit extension Animation { public static func timingCurve( diff --git a/Sources/NavigationTransition/Fade.swift b/Sources/NavigationTransition/Fade.swift index 852d3bd8..37960316 100644 --- a/Sources/NavigationTransition/Fade.swift +++ b/Sources/NavigationTransition/Fade.swift @@ -1,4 +1,4 @@ -internal import AtomicTransition +import AtomicTransition extension AnyNavigationTransition { /// A transition that fades the pushed view in, fades the popped view out, or cross-fades both views. diff --git a/Sources/NavigationTransition/Slide.swift b/Sources/NavigationTransition/Slide.swift index a56d07f9..a1138dfa 100644 --- a/Sources/NavigationTransition/Slide.swift +++ b/Sources/NavigationTransition/Slide.swift @@ -1,4 +1,4 @@ -internal import AtomicTransition +import AtomicTransition public import SwiftUI extension AnyNavigationTransition { diff --git a/Sources/RuntimeAssociation/RuntimeAssociation.swift b/Sources/RuntimeAssociation/RuntimeAssociation.swift deleted file mode 100644 index ad4573d5..00000000 --- a/Sources/RuntimeAssociation/RuntimeAssociation.swift +++ /dev/null @@ -1,20 +0,0 @@ -import ObjectiveC - -public protocol RuntimeAssociation: AnyObject { - subscript(forKey key: String, policy: RuntimeAssociationPolicy) -> T? { get set } -} - -extension RuntimeAssociation { - public subscript(forKey key: String = #function, policy: RuntimeAssociationPolicy = .retain(.nonatomic)) -> T? { - get { - let key = unsafeBitCast(Selector(key), to: UnsafeRawPointer.self) - return objc_getAssociatedObject(self, key) as? T - } - set { - let key = unsafeBitCast(Selector(key), to: UnsafeRawPointer.self) - objc_setAssociatedObject(self, key, newValue, .init(policy)) - } - } -} - -extension NSObject: RuntimeAssociation {} diff --git a/Sources/RuntimeAssociation/RuntimeAssociationPolicy.swift b/Sources/RuntimeAssociation/RuntimeAssociationPolicy.swift deleted file mode 100644 index dddad10b..00000000 --- a/Sources/RuntimeAssociation/RuntimeAssociationPolicy.swift +++ /dev/null @@ -1,29 +0,0 @@ -import ObjectiveC - -public enum RuntimeAssociationPolicy { - public enum Atomicity { - case atomic - case nonatomic - } - - case assign - case copy(Atomicity) - case retain(Atomicity) -} - -extension objc_AssociationPolicy { - init(_ policy: RuntimeAssociationPolicy) { - switch policy { - case .assign: - self = .OBJC_ASSOCIATION_ASSIGN - case .copy(.atomic): - self = .OBJC_ASSOCIATION_COPY - case .copy(.nonatomic): - self = .OBJC_ASSOCIATION_COPY_NONATOMIC - case .retain(.atomic): - self = .OBJC_ASSOCIATION_RETAIN - case .retain(.nonatomic): - self = .OBJC_ASSOCIATION_RETAIN_NONATOMIC - } - } -} diff --git a/Sources/RuntimeSwizzling/Swizzle.swift b/Sources/RuntimeSwizzling/Swizzle.swift deleted file mode 100644 index fec53362..00000000 --- a/Sources/RuntimeSwizzling/Swizzle.swift +++ /dev/null @@ -1,49 +0,0 @@ -public import ObjectiveC - -public var swizzleLogs = false - -public func swizzle(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) { - guard !swizzlingHistory.contains(type, original, swizzled) else { - return - } - - swizzlingHistory.add(type, original, swizzled) - - guard let originalMethod = class_getInstanceMethod(type, original) else { - assertionFailure("[Swizzling] Instance method \(type).\(original) not found.") - return - } - - guard let swizzledMethod = class_getInstanceMethod(type, swizzled) else { - assertionFailure("[Swizzling] Instance method \(type).\(swizzled) not found.") - return - } - - if swizzleLogs { - print("[Swizzling] [\(type) \(original) <~> \(swizzled)]") - } - - method_exchangeImplementations(originalMethod, swizzledMethod) -} - -private struct SwizzlingHistory { - private var map: [Int: Void] = [:] - - func contains(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) -> Bool { - map[hash(type, original, swizzled)] != nil - } - - mutating func add(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) { - map[hash(type, original, swizzled)] = () - } - - private func hash(_ type: AnyObject.Type, _ original: Selector, _ swizzled: Selector) -> Int { - var hasher = Hasher() - hasher.combine(ObjectIdentifier(type)) - hasher.combine(original) - hasher.combine(swizzled) - return hasher.finalize() - } -} - -private var swizzlingHistory = SwizzlingHistory() diff --git a/Sources/SwiftUINavigationTransitions/SwiftUISupport.swift b/Sources/SwiftUINavigationTransitions/SwiftUISupport.swift index eff2f1e8..bb4bd89d 100644 --- a/Sources/SwiftUINavigationTransitions/SwiftUISupport.swift +++ b/Sources/SwiftUINavigationTransitions/SwiftUISupport.swift @@ -1,6 +1,6 @@ public import UIKitNavigationTransitions public import SwiftUI -@_spi(Advanced) internal import SwiftUIIntrospect +@_spi(Advanced) import SwiftUIIntrospect extension View { @MainActor diff --git a/Sources/TestUtils/AnimatorTransientView+Mocks.swift b/Sources/TestUtils/AnimatorTransientView+Mocks.swift index 5a28d341..b3430b7d 100644 --- a/Sources/TestUtils/AnimatorTransientView+Mocks.swift +++ b/Sources/TestUtils/AnimatorTransientView+Mocks.swift @@ -1,6 +1,6 @@ @testable public import Animator -internal import UIKit import IssueReporting +import UIKit extension AnimatorTransientView { public static var unimplemented: AnimatorTransientView { diff --git a/Sources/UIKitNavigationTransitions/Delegate.swift b/Sources/UIKitNavigationTransitions/Delegate.swift index 66adc3a4..d484453a 100644 --- a/Sources/UIKitNavigationTransitions/Delegate.swift +++ b/Sources/UIKitNavigationTransitions/Delegate.swift @@ -1,7 +1,7 @@ -internal import Animation -internal import Animator -internal import NavigationTransition -internal import UIKit +import Animation +import Animator +import NavigationTransition +import UIKit final class NavigationTransitionDelegate: NSObject, UINavigationControllerDelegate { var transition: AnyNavigationTransition diff --git a/Sources/UIKitNavigationTransitions/Interaction.swift b/Sources/UIKitNavigationTransitions/Interaction.swift index 9c930018..84cd1718 100644 --- a/Sources/UIKitNavigationTransitions/Interaction.swift +++ b/Sources/UIKitNavigationTransitions/Interaction.swift @@ -1,4 +1,4 @@ -internal import UIKit +import UIKit extension UINavigationController { @objc func handleInteraction(_ gestureRecognizer: UIPanGestureRecognizer) { diff --git a/Sources/UIKitNavigationTransitions/UIKitSupport.swift b/Sources/UIKitNavigationTransitions/UIKitSupport.swift index 6eced620..ffae88ea 100644 --- a/Sources/UIKitNavigationTransitions/UIKitSupport.swift +++ b/Sources/UIKitNavigationTransitions/UIKitSupport.swift @@ -1,6 +1,7 @@ +import IssueReporting public import NavigationTransition -import RuntimeAssociation -import RuntimeSwizzling +import ObjCRuntimeTools +import Once public import UIKit public struct UISplitViewControllerColumns: OptionSet { @@ -105,16 +106,13 @@ extension RandomAccessCollection where Index == Int { } extension UINavigationController { - private var defaultDelegate: (any UINavigationControllerDelegate)! { - get { self[] } - set { self[] = newValue } - } + @Associated(.retain(.nonatomic)) + private var defaultDelegate: (any UINavigationControllerDelegate)! - var customDelegate: NavigationTransitionDelegate! { - get { self[] } - set { - self[] = newValue - delegate = newValue + @Associated(.retain(.nonatomic)) + var customDelegate: NavigationTransitionDelegate? { + didSet { + delegate = customDelegate } } @@ -122,46 +120,22 @@ extension UINavigationController { _ transition: AnyNavigationTransition, interactivity: AnyNavigationTransition.Interactivity = .default ) { + do { + try UINavigationController.swizzle() + } catch { + reportIssue(error, "Failed to swizzle required UINavigationController methods") + } + if defaultDelegate == nil { defaultDelegate = delegate } - if customDelegate == nil { - customDelegate = NavigationTransitionDelegate(transition: transition, baseDelegate: defaultDelegate) - } else { + if let customDelegate { customDelegate.transition = transition + } else { + customDelegate = NavigationTransitionDelegate(transition: transition, baseDelegate: defaultDelegate) } - swizzle( - UINavigationController.self, - #selector(UINavigationController.setViewControllers), - #selector(UINavigationController.setViewControllers_animateIfNeeded) - ) - - swizzle( - UINavigationController.self, - #selector(UINavigationController.pushViewController), - #selector(UINavigationController.pushViewController_animateIfNeeded) - ) - - swizzle( - UINavigationController.self, - #selector(UINavigationController.popViewController), - #selector(UINavigationController.popViewController_animateIfNeeded) - ) - - swizzle( - UINavigationController.self, - #selector(UINavigationController.popToViewController), - #selector(UINavigationController.popToViewController_animateIfNeeded) - ) - - swizzle( - UINavigationController.self, - #selector(UINavigationController.popToRootViewController), - #selector(UINavigationController.popToRootViewController_animateIfNeeded) - ) - #if !os(tvOS) && !os(visionOS) if defaultEdgePanRecognizer.strongDelegate == nil { defaultEdgePanRecognizer.strongDelegate = NavigationGestureRecognizerDelegate(controller: self) @@ -211,6 +185,68 @@ extension UINavigationController { #endif } + private static func swizzle() throws { + try #once { + try #swizzle( + UINavigationController.setViewControllers, + params: [UIViewController].self, Bool.self + ) { $self, viewControllers, animated in + if let transitionDelegate = self.customDelegate { + self.setViewControllers(viewControllers, animated: transitionDelegate.transition.animation != nil) + } else { + self.setViewControllers(viewControllers, animated: animated) + } + } + + try #swizzle( + UINavigationController.pushViewController, + params: UIViewController.self, Bool.self + ) { $self, viewController, animated in + if let transitionDelegate = self.customDelegate { + self.pushViewController(viewController, animated: transitionDelegate.transition.animation != nil) + } else { + self.pushViewController(viewController, animated: animated) + } + } + + try #swizzle( + UINavigationController.popViewController, + params: Bool.self, + returning: UIViewController?.self + ) { $self, animated in + if let transitionDelegate = self.customDelegate { + self.popViewController(animated: transitionDelegate.transition.animation != nil) + } else { + self.popViewController(animated: animated) + } + } + + try #swizzle( + UINavigationController.popToViewController, + params: UIViewController.self, Bool.self, + returning: [UIViewController]?.self + ) { $self, viewController, animated in + if let transitionDelegate = self.customDelegate { + self.popToViewController(viewController, animated: transitionDelegate.transition.animation != nil) + } else { + self.popToViewController(viewController, animated: animated) + } + } + + try #swizzle( + UINavigationController.popToRootViewController, + params: Bool.self, + returning: [UIViewController]?.self + ) { $self, animated in + if let transitionDelegate = self.customDelegate { + self.popToRootViewController(animated: transitionDelegate.transition.animation != nil) + } else { + self.popToRootViewController(animated: animated) + } + } + } + } + @available(tvOS, unavailable) @available(visionOS, unavailable) private func exclusivelyEnableGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer?) { @@ -224,48 +260,6 @@ extension UINavigationController { } } -extension UINavigationController { - @objc private func setViewControllers_animateIfNeeded(_ viewControllers: [UIViewController], animated: Bool) { - if let transitionDelegate = customDelegate { - setViewControllers_animateIfNeeded(viewControllers, animated: transitionDelegate.transition.animation != nil) - } else { - setViewControllers_animateIfNeeded(viewControllers, animated: animated) - } - } - - @objc private func pushViewController_animateIfNeeded(_ viewController: UIViewController, animated: Bool) { - if let transitionDelegate = customDelegate { - pushViewController_animateIfNeeded(viewController, animated: transitionDelegate.transition.animation != nil) - } else { - pushViewController_animateIfNeeded(viewController, animated: animated) - } - } - - @objc private func popViewController_animateIfNeeded(animated: Bool) -> UIViewController? { - if let transitionDelegate = customDelegate { - popViewController_animateIfNeeded(animated: transitionDelegate.transition.animation != nil) - } else { - popViewController_animateIfNeeded(animated: animated) - } - } - - @objc private func popToViewController_animateIfNeeded(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? { - if let transitionDelegate = customDelegate { - popToViewController_animateIfNeeded(viewController, animated: transitionDelegate.transition.animation != nil) - } else { - popToViewController_animateIfNeeded(viewController, animated: animated) - } - } - - @objc private func popToRootViewController_animateIfNeeded(animated: Bool) -> UIViewController? { - if let transitionDelegate = customDelegate { - popToRootViewController_animateIfNeeded(animated: transitionDelegate.transition.animation != nil) - } else { - popToRootViewController_animateIfNeeded(animated: animated) - } - } -} - @available(tvOS, unavailable) @available(visionOS, unavailable) extension UINavigationController { @@ -273,29 +267,22 @@ extension UINavigationController { interactivePopGestureRecognizer as? UIScreenEdgePanGestureRecognizer } - var defaultPanRecognizer: UIPanGestureRecognizer! { - get { self[] } - set { self[] = newValue } - } + @Associated(.retain(.nonatomic)) + var defaultPanRecognizer: UIPanGestureRecognizer! - var edgePanRecognizer: UIScreenEdgePanGestureRecognizer! { - get { self[] } - set { self[] = newValue } - } + @Associated(.retain(.nonatomic)) + var edgePanRecognizer: UIScreenEdgePanGestureRecognizer! - var panRecognizer: UIPanGestureRecognizer! { - get { self[] } - set { self[] = newValue } - } + @Associated(.retain(.nonatomic)) + var panRecognizer: UIPanGestureRecognizer! } @available(tvOS, unavailable) extension UIGestureRecognizer { + @Associated(.retain(.nonatomic)) var strongDelegate: (any UIGestureRecognizerDelegate)? { - get { self[] } - set { - self[] = newValue - delegate = newValue + didSet { + delegate = strongDelegate } }