diff --git a/.github/package.xcworkspace/contents.xcworkspacedata b/.github/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..0fd0bc3
--- /dev/null
+++ b/.github/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 0000000..0368460
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,41 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-macro-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-macro-testing.git",
+ "state" : {
+ "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35",
+ "version" : "0.2.2"
+ }
+ },
+ {
+ "identity" : "swift-macro-toolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/stackotter/swift-macro-toolkit.git",
+ "state" : {
+ "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576",
+ "version" : "0.3.1"
+ }
+ },
+ {
+ "identity" : "swift-snapshot-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
+ "state" : {
+ "revision" : "8e68404f641300bfd0e37d478683bb275926760c",
+ "version" : "1.15.2"
+ }
+ },
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-syntax.git",
+ "state" : {
+ "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
+ "version" : "509.0.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme
new file mode 100644
index 0000000..56e193c
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/xcschemes/Interception.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme
new file mode 100644
index 0000000..2c211d8
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/xcschemes/InterceptionTests.xcscheme
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme
new file mode 100644
index 0000000..1695afb
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme
new file mode 100644
index 0000000..352ae6c
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme
new file mode 100644
index 0000000..2127e55
--- /dev/null
+++ b/.github/package.xcworkspace/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c54d8cb
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - '*'
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ library-swift-latest:
+ name: Library
+ if: |
+ !contains(github.event.head_commit.message, '[ci skip]') &&
+ !contains(github.event.head_commit.message, '[ci skip test]') &&
+ !contains(github.event.head_commit.message, '[ci skip library-swift-latest]')
+ runs-on: macos-13
+ timeout-minutes: 30
+ strategy:
+ matrix:
+ config:
+ - debug
+ - release
+ steps:
+ - uses: actions/checkout@v4
+ - name: Select Xcode 15.1
+ run: sudo xcode-select -s /Applications/Xcode_15.1.app
+ - name: Run tests
+ run: make CONFIG=debug test-library
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0023a53
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..ef0bb96
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,5 @@
+version: 1
+builder:
+ configs:
+ - documentation_targets: [Interception]
+ swift_version: 5.9
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme
new file mode 100644
index 0000000..56e193c
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Interception.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme
new file mode 100644
index 0000000..2c211d8
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/InterceptionTests.xcscheme
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme
new file mode 100644
index 0000000..1695afb
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectors.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme
new file mode 100644
index 0000000..352ae6c
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionCustomSelectorsMacrosTests.xcscheme
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme
new file mode 100644
index 0000000..2127e55
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/_SwiftInterceptionUtils.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ACKNOWLEDGMENTS b/ACKNOWLEDGMENTS
new file mode 100644
index 0000000..7c348e2
--- /dev/null
+++ b/ACKNOWLEDGMENTS
@@ -0,0 +1,83 @@
+Inspiration and some implementations are taken from:
+
+––––––––––––––––––––––––––––––
+https://github.com/ReactiveCocoa/ReactiveSwift
+
+LICENCE
+------------------------------
+Copyright (c) 2012 - 2016, GitHub, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+––––––––––––––––––––––––––––––
+
+––––––––––––––––––––––––––––––
+https://github.com/ReactiveCocoa/ReactiveCocoa
+
+LICENCE
+------------------------------
+Copyright (c) 2012 - 2016, GitHub, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+––––––––––––––––––––––––––––––
+
+––––––––––––––––––––––––––––––
+https://github.com/CombineCommunity/CombineExt
+
+LICENCE
+------------------------------
+Copyright (c) 2020 Combine Community, and/or Shai Mishali
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+––––––––––––––––––––––––––––––
+
+––––––––––––––––––––––––––––––
+https://github.com/CombineCommunity/CombineCocoa
+
+LICENCE
+------------------------------
+MIT License
+
+Copyright (c) 2019 Shai Mishali
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+––––––––––––––––––––––––––––––
diff --git a/LICENCE.md b/LICENCE.md
new file mode 100644
index 0000000..929aa26
--- /dev/null
+++ b/LICENCE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 CaptureContext
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ab85d0e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,42 @@
+CONFIG = debug
+PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS 17,iPhone \d\+ Pro [^M])
+PLATFORM_MACOS = macOS
+PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst
+PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS 17,TV)
+PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS 10,Watch)
+
+default: test
+
+test:
+ $(MAKE) CONFIG=debug test-library
+ $(MAKE) test-docs
+
+test-library:
+ for platform in "$(PLATFORM_IOS)" "$(PLATFORM_MACOS)" "$(PLATFORM_MAC_CATALYST)" "$(PLATFORM_TVOS)" "$(PLATFORM_WATCHOS)"; do \
+ echo "\nTesting Library on $$platform\n" && \
+ (xcodebuild test \
+ -skipMacroValidation \
+ -configuration $(CONFIG) \
+ -workspace .github/package.xcworkspace \
+ -scheme InterceptionTests \
+ -destination platform="$$platform" | xcpretty && exit 0 \
+ ) \
+ || exit 1; \
+ done;
+
+DOC_WARNINGS = $(shell xcodebuild clean docbuild \
+ -scheme Interception \
+ -destination platform="$(PLATFORM_IOS)" \
+ -quiet \
+ 2>&1 \
+ | grep "couldn't be resolved to known documentation" \
+ | sed 's|$(PWD)|.|g' \
+ | tr '\n' '\1')
+test-docs:
+ @test "$(DOC_WARNINGS)" = "" \
+ || (echo "xcodebuild docbuild failed:\n\n$(DOC_WARNINGS)" | tr '\1' '\n' \
+ && exit 1)
+
+define udid_for
+$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }')
+endef
diff --git a/Package.resolved b/Package.resolved
new file mode 100644
index 0000000..0368460
--- /dev/null
+++ b/Package.resolved
@@ -0,0 +1,41 @@
+{
+ "pins" : [
+ {
+ "identity" : "swift-macro-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-macro-testing.git",
+ "state" : {
+ "revision" : "10dcef36314ddfea6f60442169b0b320204cbd35",
+ "version" : "0.2.2"
+ }
+ },
+ {
+ "identity" : "swift-macro-toolkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/stackotter/swift-macro-toolkit.git",
+ "state" : {
+ "revision" : "106daeb38eb3f52b1540aed981fc63fa22274576",
+ "version" : "0.3.1"
+ }
+ },
+ {
+ "identity" : "swift-snapshot-testing",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
+ "state" : {
+ "revision" : "8e68404f641300bfd0e37d478683bb275926760c",
+ "version" : "1.15.2"
+ }
+ },
+ {
+ "identity" : "swift-syntax",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-syntax.git",
+ "state" : {
+ "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
+ "version" : "509.0.0"
+ }
+ }
+ ],
+ "version" : 2
+}
diff --git a/Package.swift b/Package.swift
new file mode 100644
index 0000000..abe52cf
--- /dev/null
+++ b/Package.swift
@@ -0,0 +1,88 @@
+// swift-tools-version: 5.9
+
+import PackageDescription
+import CompilerPluginSupport
+
+let package = Package(
+ name: "swift-interception",
+ platforms: [
+ .iOS(.v13),
+ .macOS(.v10_15),
+ .tvOS(.v13),
+ .macCatalyst(.v13),
+ .watchOS(.v6)
+ ],
+ products: [
+ .library(
+ name: "_SwiftInterceptionCustomSelectors",
+ type: .static,
+ targets: ["_SwiftInterceptionCustomSelectors"]
+ ),
+ .library(
+ name: "_SwiftInterceptionUtils",
+ type: .static,
+ targets: ["_SwiftInterceptionUtils"]
+ ),
+ .library(
+ name: "Interception",
+ type: .static,
+ targets: ["Interception"]
+ )
+ ],
+ dependencies: [
+ .package(
+ url: "https://github.com/stackotter/swift-macro-toolkit.git",
+ .upToNextMinor(from: "0.3.0")
+ ),
+ .package(
+ url: "https://github.com/pointfreeco/swift-macro-testing.git",
+ .upToNextMinor(from: "0.2.2")
+ )
+ ],
+ targets: [
+ .macro(
+ name: "_SwiftInterceptionCustomSelectorsMacros",
+ dependencies: [
+ .product(
+ name: "MacroToolkit",
+ package: "swift-macro-toolkit"
+ )
+ ]
+ ),
+
+ .target(
+ name: "_SwiftInterceptionCustomSelectors",
+ dependencies: [
+ .target(name: "_SwiftInterceptionCustomSelectorsMacros"),
+ ]
+ ),
+ .target(name: "_SwiftInterceptionUtilsObjc"),
+ .target(
+ name: "_SwiftInterceptionUtils",
+ dependencies: [
+ .target(name: "_SwiftInterceptionUtilsObjc"),
+ ]
+ ),
+ .target(
+ name: "Interception",
+ dependencies: [
+ .target(name: "_SwiftInterceptionCustomSelectors"),
+ .target(name: "_SwiftInterceptionUtils"),
+ ]
+ ),
+
+ .testTarget(
+ name: "_SwiftInterceptionCustomSelectorsMacrosTests",
+ dependencies: [
+ .target(name: "_SwiftInterceptionCustomSelectorsMacros"),
+ .product(name: "MacroTesting", package: "swift-macro-testing"),
+ ]
+ ),
+ .testTarget(
+ name: "InterceptionTests",
+ dependencies: [
+ .target(name: "Interception"),
+ ]
+ ),
+ ]
+)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4251945
--- /dev/null
+++ b/README.md
@@ -0,0 +1,92 @@
+# swift-interception
+
+[![SwiftPM 5.9](https://img.shields.io/badge/swiftpm-5.9-ED523F.svg?style=flat)](https://swift.org/download/) ![Platforms](https://img.shields.io/badge/Platforms-iOS_13_|_macOS_10.15_|_Catalyst_13_|_tvOS_13_|_watchOS_7-ED523F.svg?style=flat) [![@capturecontext](https://img.shields.io/badge/contact-@capturecontext-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context)
+
+Package for interception of objc selectors in Swift.
+
+## Usage
+
+### Basic
+
+Observe any selectors on NSObject instances
+
+```swift
+import Interception
+
+navigationController.setInterceptionHandler(
+ for: #methodSelector(UINavigationController.popViewController)
+) { result in
+ print(result.args) // `animated` flag
+ print(result.output) // popped `UIViewController?``
+}
+```
+
+You can set up multiple interception handlers as well, just make sure that you use different keys for each handler
+
+```swift
+import Interception
+
+object.setInterceptionHandler(
+ for: #methodSelector(MyObject.someMethod(arg1:arg2)),
+ key: "argumentsPrinter"
+) { result in
+ // In case of multiple arguments
+ // you can access them as a tuple
+ print(result.args.0)
+ print(result.args.1)
+}
+```
+
+### Library development
+
+If you use it to create a library it may be a good idea to export custom selectors implicitly
+
+```swift
+// Exports.swift
+@_exported import _SwiftInterceptionCustomSelectors
+```
+
+Also you may find some `@_spi` methods and Utils helpful
+
+```swift
+@_spi(Internals) import Interception
+import _SwiftInterceptionUtils // Is not shown in the autocomplete
+```
+
+
+
+## Installation
+
+### Basic
+
+You can add CombineInterception to an Xcode project by adding it as a package dependency.
+
+1. From the **File** menu, select **Swift Packages › Add Package Dependency…**
+2. Enter [`"https://github.com/capturecontext/swift-interception.git"`](https://github.com/capturecontext/swift-interception.git) into the package repository URL text field
+3. Choose products you need to link them to your project.
+
+### Recommended
+
+If you use SwiftPM for your project, you can add CombineInterception to your package file.
+
+```swift
+.package(
+ url: "https://github.com/capturecontext/swift-interception.git",
+ branch: "main"
+)
+```
+
+Do not forget about target dependencies:
+
+```swift
+.product(
+ name: "Interception",
+ package: "swift-interception"
+)
+```
+
+## License
+
+This library is released under the MIT license. See [LICENCE](LICENCE) for details.
+
+See [ACKNOWLEDGMENTS](ACKNOWLEDGMENTS) for inspiration references and their licences.
diff --git a/Sources/Interception/Exports.swift b/Sources/Interception/Exports.swift
new file mode 100644
index 0000000..0a97b7c
--- /dev/null
+++ b/Sources/Interception/Exports.swift
@@ -0,0 +1 @@
+@_exported import _SwiftInterceptionCustomSelectors
diff --git a/Sources/Interception/InterceptionResult.swift b/Sources/Interception/InterceptionResult.swift
new file mode 100644
index 0000000..9379ca3
--- /dev/null
+++ b/Sources/Interception/InterceptionResult.swift
@@ -0,0 +1,60 @@
+public struct InterceptionResult {
+ public var args: Args
+ public var output: Output
+
+ @_spi(Internals)
+ public init(args: Args, output: Output) {
+ self.args = args
+ self.output = output
+ }
+}
+
+extension InterceptionResult where Args == [Any?], Output == Any? {
+ @_spi(Internals)
+ public func unsafeCast(
+ args: (repeat each Arg).Type = (repeat each Arg).self,
+ output: T.Type = T.self
+ ) -> InterceptionResult<(repeat each Arg), T> {
+ return .init(
+ args: self.args.unsafeCastToTuple(of: args),
+ output: self.output as! T
+ )
+ }
+}
+
+extension Array {
+ @_spi(Internals)
+ public func unsafeCastToTuple(
+ of type: (repeat each T).Type = (repeat each T).self
+ ) -> (repeat each T) {
+ guard (repeat each T).self != Void.self else {
+ return unsafeBitCast((), to: (repeat each T).self)
+ }
+
+ guard self.count > 1 else {
+ return forceCast(self.first!, to: (repeat each T).self)
+ }
+
+ var copy = self
+ var tuple: Any = copy.removeLast()
+ for _ in 1..(_ value: From, to type: Any.Type) -> To {
+ let forceCastBox: (Any) -> To = { unboxedValue in
+ guard let castedValue = unboxedValue as? To else {
+ fatalError("Could not cast \(From.self) to \(To.self)")
+ }
+
+ return castedValue
+ }
+
+ let box = unsafeBitCast(forceCastBox, to: ((From) -> To).self)
+
+ return box(value)
+}
diff --git a/Sources/Interception/NSObject+Interception.swift b/Sources/Interception/NSObject+Interception.swift
new file mode 100644
index 0000000..a6bf64c
--- /dev/null
+++ b/Sources/Interception/NSObject+Interception.swift
@@ -0,0 +1,533 @@
+#if canImport(Combine)
+import Foundation
+import _SwiftInterceptionUtils
+import _SwiftInterceptionCustomSelectors
+
+/// Whether the runtime subclass has already been prepared for method
+/// interception.
+private let interceptedKey = AssociationKey(default: false)
+
+/// Holds the method signature cache of the runtime subclass.
+private let signatureCacheKey = AssociationKey()
+
+/// Holds the method selector cache of the runtime subclass.
+private let selectorCacheKey = AssociationKey()
+
+internal let noImplementation: IMP = unsafeBitCast(Int(0), to: IMP.self)
+
+extension NSObject {
+ /// Sets interception handler, which accepts ``InterceptionResult`` containing a tuple
+ /// of bridged arguments and bridged output, at the end of every invocation of `selector` on the object.
+ ///
+ /// > ⚠️ Handlers should not call the method specified by the selector.
+ ///
+ /// - Parameters:
+ /// - selector: The selector to observe.
+ /// - key: Registration key for the interception handler
+ /// - action: Action to perform when `selector` was triggered
+ public func setInterceptionHandler(
+ for selector: Selector,
+ key: AnyHashable = "__default",
+ action: ((InterceptionResult<[Any?], Any?>) -> Void)?
+ ) {
+ let handler = _intercept(selector)
+ handler.register(action, for: key)
+ }
+
+ /// Sets interception handler, which accepts ``InterceptionResult`` containing a tuple
+ /// of casted arguments and casted output, at the end of every invocation of `selector` on the object.
+ ///
+ /// > ⚠️ Handlers should not call the method specified by the selector.
+ ///
+ /// - Parameters:
+ /// - selector: The selector to observe.
+ /// - key: Registration key for the interception handler
+ /// - action: Action to perform when `selector` was triggered
+ public func setInterceptionHandler(
+ for selector: _MethodSelector,
+ key: AnyHashable = "__default",
+ action: ((InterceptionResult) -> Void)?
+ ) {
+ let handler = _intercept(selector.wrappedValue)
+
+ guard let action else {
+ handler.register(nil, for: key)
+ return
+ }
+
+ handler.register({ action($0.unsafeCast()) }, for: key)
+ }
+
+ /// Setup the method interception.
+ @nonobjc fileprivate func _intercept(_ selector: Selector) -> InterceptionHanlders {
+ guard let method = class_getInstanceMethod(objcClass, selector) else {
+ fatalError(
+ "Selector `\(selector)` does not exist in class `\(String(describing: objcClass))`."
+ )
+ }
+
+ let typeEncoding = method_getTypeEncoding(method)!
+ assert(checkTypeEncoding(typeEncoding))
+
+ return synchronized(self) {
+ let alias = selector.alias
+ let handlerKey = AssociationKey(alias)
+ let interopAlias = selector.interopAlias
+
+ if let handler = associations.value(forKey: handlerKey) {
+ return handler
+ }
+
+ let subclass: AnyClass = swizzleClass(self)
+ let subclassAssociations = Associations(subclass as AnyObject)
+
+ synchronized(subclass) {
+ let isSwizzled = subclassAssociations.value(forKey: interceptedKey)
+
+ let signatureCache: SignatureCache
+ let selectorCache: SelectorCache
+
+ if isSwizzled {
+ signatureCache = subclassAssociations.value(forKey: signatureCacheKey)
+ selectorCache = subclassAssociations.value(forKey: selectorCacheKey)
+ }
+ else {
+ signatureCache = SignatureCache()
+ selectorCache = SelectorCache()
+
+ subclassAssociations.setValue(signatureCache, forKey: signatureCacheKey)
+ subclassAssociations.setValue(selectorCache, forKey: selectorCacheKey)
+ subclassAssociations.setValue(true, forKey: interceptedKey)
+
+ enableMessageForwarding(subclass, selectorCache)
+ setupMethodSignatureCaching(subclass, signatureCache)
+ }
+
+ selectorCache.cache(selector)
+
+ if signatureCache[selector] == nil {
+ let signature = NSMethodSignature.objcSignature(withObjCTypes: typeEncoding)
+ signatureCache[selector] = signature
+ }
+
+ // If an immediate implementation of the selector is found in the
+ // runtime subclass the first time the selector is intercepted,
+ // preserve the implementation.
+ //
+ // Example: KVO setters if the instance is swizzled by KVO before combine-interception
+ // does.
+ if !class_respondsToSelector(subclass, interopAlias) {
+ let immediateImpl = class_getImmediateMethod(subclass, selector)
+ .flatMap(method_getImplementation)
+ .flatMap { $0 != _swiftInterceptionMsgForward ? $0 : nil }
+
+ if let impl = immediateImpl {
+ let succeeds = class_addMethod(subclass, interopAlias, impl, typeEncoding)
+ precondition(
+ succeeds,
+ "combine-interception attempts to swizzle a selector that has message forwarding enabled with a runtime injected implementation. This is unsupported in the current version."
+ )
+ }
+ }
+ }
+
+ let handler = InterceptionHanlders()
+ associations.setValue(handler, forKey: handlerKey)
+
+ // Start forwarding the messages of the selector.
+ _ = class_replaceMethod(subclass, selector, _swiftInterceptionMsgForward, typeEncoding)
+
+ return handler
+ }
+ }
+}
+
+/// Swizzle `realClass` to enable message forwarding for method interception.
+///
+/// - parameters:
+/// - realClass: The runtime subclass to be swizzled.
+private func enableMessageForwarding(_ realClass: AnyClass, _ selectorCache: SelectorCache) {
+ let perceivedClass: AnyClass = class_getSuperclass(realClass)!
+
+ typealias ForwardInvocationImpl = @convention(block) (Unmanaged, AnyObject) -> Void
+ let newForwardInvocation: ForwardInvocationImpl = { objectRef, invocation in
+ let selector = invocation.selector!
+ let alias = selectorCache.alias(for: selector)
+ let interopAlias = selectorCache.interopAlias(for: selector)
+
+ defer {
+ let handlerKey = AssociationKey(alias)
+ if let handler = objectRef.takeUnretainedValue().associations.value(forKey: handlerKey) {
+ handler(invocation)
+ }
+ }
+
+ let method = class_getInstanceMethod(perceivedClass, selector)
+ let typeEncoding: String
+
+ if let runtimeTypeEncoding = method.flatMap(method_getTypeEncoding) {
+ typeEncoding = String(cString: runtimeTypeEncoding)
+ }
+ else {
+ let methodSignature = (objectRef.takeUnretainedValue() as AnyObject)
+ .objcMethodSignature(for: selector)
+ let encodings = (0.., Selector, AnyObject) ->
+ Void
+ let forwardInvocationImpl = class_getMethodImplementation(
+ perceivedClass,
+ ObjCSelector.forwardInvocation
+ )
+ let forwardInvocation = unsafeBitCast(forwardInvocationImpl, to: SuperForwardInvocation.self)
+ forwardInvocation(objectRef, ObjCSelector.forwardInvocation, invocation)
+ }
+
+ _ = class_replaceMethod(
+ realClass,
+ ObjCSelector.forwardInvocation,
+ imp_implementationWithBlock(newForwardInvocation as Any),
+ ObjCMethodEncoding.forwardInvocation
+ )
+}
+
+/// Swizzle `realClass` to accelerate the method signature retrieval, using a
+/// signature cache that covers all known intercepted selectors of `realClass`.
+///
+/// - parameters:
+/// - realClass: The runtime subclass to be swizzled.
+/// - signatureCache: The method signature cache.
+private func setupMethodSignatureCaching(_ realClass: AnyClass, _ signatureCache: SignatureCache) {
+ let perceivedClass: AnyClass = class_getSuperclass(realClass)!
+
+ let newMethodSignatureForSelector:
+ @convention(block) (Unmanaged, Selector) -> AnyObject? = { objectRef, selector in
+ if let signature = signatureCache[selector] {
+ return signature
+ }
+
+ typealias SuperMethodSignatureForSelector = @convention(c) (
+ Unmanaged, Selector, Selector
+ ) -> AnyObject?
+ let impl = class_getMethodImplementation(
+ perceivedClass,
+ ObjCSelector.methodSignatureForSelector
+ )
+ let methodSignatureForSelector = unsafeBitCast(impl, to: SuperMethodSignatureForSelector.self)
+ return methodSignatureForSelector(
+ objectRef,
+ ObjCSelector.methodSignatureForSelector,
+ selector
+ )
+ }
+
+ _ = class_replaceMethod(
+ realClass,
+ ObjCSelector.methodSignatureForSelector,
+ imp_implementationWithBlock(newMethodSignatureForSelector as Any),
+ ObjCMethodEncoding.methodSignatureForSelector
+ )
+}
+
+/// The state of an intercepted method specific to an instance.
+private final class InterceptionHanlders {
+ typealias Action = (InterceptionResult<[Any?], Any?>) -> Void
+ private var handlers: [AnyHashable: Action] = [:]
+
+ func register(_ action: Action?, for key: AnyHashable) {
+ handlers[key] = action
+ }
+
+ func callAsFunction(_ invocation: AnyObject) {
+ let unpackedInvocation = unpackInvocation(invocation)
+ handlers.values.forEach { $0(unpackedInvocation) }
+ }
+}
+
+private final class SelectorCache {
+ private var map: [Selector: (main: Selector, interop: Selector)] = [:]
+
+ init() {}
+
+ /// Cache the aliases of the specified selector in the cache.
+ ///
+ /// - warning: Any invocation of this method must be synchronized against the
+ /// runtime subclass.
+ @discardableResult
+ func cache(_ selector: Selector) -> (main: Selector, interop: Selector) {
+ if let pair = map[selector] {
+ return pair
+ }
+
+ let aliases = (selector.alias, selector.interopAlias)
+ map[selector] = aliases
+
+ return aliases
+ }
+
+ /// Get the alias of the specified selector.
+ ///
+ /// - parameters:
+ /// - selector: The selector alias.
+ func alias(for selector: Selector) -> Selector {
+ if let (main, _) = map[selector] {
+ return main
+ }
+
+ return selector.alias
+ }
+
+ /// Get the secondary alias of the specified selector.
+ ///
+ /// - parameters:
+ /// - selector: The selector alias.
+ func interopAlias(for selector: Selector) -> Selector {
+ if let (_, interop) = map[selector] {
+ return interop
+ }
+
+ return selector.interopAlias
+ }
+}
+
+// The signature cache for classes that have been swizzled for method
+// interception.
+//
+// Read-copy-update is used here, since the cache has multiple readers but only
+// one writer.
+private final class SignatureCache {
+ // `Dictionary` takes 8 bytes for the reference to its storage and does CoW.
+ // So it should not encounter any corrupted, partially updated state.
+ private var map: [Selector: AnyObject] = [:]
+
+ init() {}
+
+ /// Get or set the signature for the specified selector.
+ ///
+ /// - warning: Any invocation of the setter must be synchronized against the
+ /// runtime subclass.
+ ///
+ /// - parameters:
+ /// - selector: The method signature.
+ subscript(selector: Selector) -> AnyObject? {
+ get {
+ return map[selector]
+ }
+ set {
+ if map[selector] == nil {
+ map[selector] = newValue
+ }
+ }
+ }
+}
+
+/// Assert that the method does not contain types that cannot be intercepted.
+///
+/// - parameters:
+/// - types: The type encoding C string of the method.
+///
+/// - returns: `true`.
+private func checkTypeEncoding(_ types: UnsafePointer) -> Bool {
+ // Some types, including vector types, are not encoded. In these cases the
+ // signature starts with the size of the argument frame.
+ assert(
+ types.pointee < Int8(UInt8(ascii: "1")) || types.pointee > Int8(UInt8(ascii: "9")),
+ "unknown method return type not supported in type encoding: \(String(cString: types))"
+ )
+
+ assert(types.pointee != Int8(UInt8(ascii: "(")), "union method return type not supported")
+ assert(types.pointee != Int8(UInt8(ascii: "{")), "struct method return type not supported")
+ assert(types.pointee != Int8(UInt8(ascii: "[")), "array method return type not supported")
+
+ assert(types.pointee != Int8(UInt8(ascii: "j")), "complex method return type not supported")
+
+ return true
+}
+
+/// Extract the arguments of an `NSInvocation` as an array of objects.
+///
+/// - parameters:
+/// - invocation: The `NSInvocation` to unpack.
+///
+/// - returns: An array of objects.
+private func unpackInvocation(_ invocation: AnyObject) -> InterceptionResult<[Any?], Any?> {
+ let invocation = invocation as AnyObject
+ let methodSignature = invocation.objcMethodSignature!
+ let count = methodSignature.objcNumberOfArguments!
+
+ var args = [Any?]()
+ args.reserveCapacity(Int(count - 2))
+
+ // Ignore `self` and `_cmd` at index 0 and 1.
+ for position in 2..,
+ value extractValueToBuffer: (UnsafeMutableRawPointer) -> Void
+) -> Any? {
+ let encoding = ObjCTypeEncoding(rawValue: rawEncoding.pointee) ?? .undefined
+ let value: Any?
+
+ func extract(_ type: T.Type) -> T {
+ let pointer = UnsafeMutableRawPointer.allocate(
+ byteCount: MemoryLayout.size,
+ alignment: MemoryLayout.alignment
+ )
+
+ defer {
+ pointer.deallocate()
+ }
+
+ extractValueToBuffer(pointer)
+ return pointer.assumingMemoryBound(to: type).pointee
+ }
+
+ switch encoding {
+ case .char:
+ value = NSNumber(value: extract(CChar.self))
+ case .int:
+ value = NSNumber(value: extract(CInt.self))
+ case .short:
+ value = NSNumber(value: extract(CShort.self))
+ case .long:
+ value = NSNumber(value: extract(CLong.self))
+ case .longLong:
+ value = NSNumber(value: extract(CLongLong.self))
+ case .unsignedChar:
+ value = NSNumber(value: extract(CUnsignedChar.self))
+ case .unsignedInt:
+ value = NSNumber(value: extract(CUnsignedInt.self))
+ case .unsignedShort:
+ value = NSNumber(value: extract(CUnsignedShort.self))
+ case .unsignedLong:
+ value = NSNumber(value: extract(CUnsignedLong.self))
+ case .unsignedLongLong:
+ value = NSNumber(value: extract(CUnsignedLongLong.self))
+ case .float:
+ value = NSNumber(value: extract(CFloat.self))
+ case .double:
+ value = NSNumber(value: extract(CDouble.self))
+ case .bool:
+ value = NSNumber(value: extract(CBool.self))
+ case .object:
+ value = extract((AnyObject?).self)
+ case .type:
+ value = extract((AnyClass?).self)
+ case .selector:
+ value = extract((Selector?).self)
+ case .void:
+ value = ()
+ case .undefined:
+ var size = 0
+ var alignment = 0
+ NSGetSizeAndAlignment(rawEncoding, &size, &alignment)
+ let buffer = UnsafeMutableRawPointer.allocate(byteCount: size, alignment: alignment)
+ defer { buffer.deallocate() }
+
+ extractValueToBuffer(buffer)
+ value = NSValue(bytes: buffer, objCType: rawEncoding)
+ }
+
+ return value
+}
+#endif
diff --git a/Sources/_SwiftInterceptionCustomSelectors/KeyPath+.swift b/Sources/_SwiftInterceptionCustomSelectors/KeyPath+.swift
new file mode 100644
index 0000000..88dd4bb
--- /dev/null
+++ b/Sources/_SwiftInterceptionCustomSelectors/KeyPath+.swift
@@ -0,0 +1,15 @@
+import Foundation
+
+extension KeyPath {
+ public var getterSelector: Selector? {
+ guard let property = _kvcKeyPathString else { return nil }
+ return Selector(property)
+ }
+}
+
+extension WritableKeyPath {
+ public var setterSelector: Selector? {
+ guard let property = _kvcKeyPathString else { return nil }
+ return Selector("set" + property.first!.uppercased() + String(property.dropFirst()) + ":")
+ }
+}
diff --git a/Sources/_SwiftInterceptionCustomSelectors/Macros.swift b/Sources/_SwiftInterceptionCustomSelectors/Macros.swift
new file mode 100644
index 0000000..da7934c
--- /dev/null
+++ b/Sources/_SwiftInterceptionCustomSelectors/Macros.swift
@@ -0,0 +1,72 @@
+import _SwiftInterceptionCustomSelectorsMacros
+import Foundation
+
+// MARK: - PropertySelectors
+
+@freestanding(expression)
+public macro propertySelector(
+ _: KeyPath