├── .codecov.yml ├── .github └── workflows │ ├── SwiftPM.yml │ ├── jazzy.yml │ ├── pod_lib_lint.yml │ ├── swiftlint.yml │ ├── swiftlint_analyze.yml │ └── xcodebuild.yml ├── .gitignore ├── .jazzy.yaml ├── .swiftlint.yml ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Configuration ├── Defaults-Debug.xcconfig ├── Defaults-Release.xcconfig ├── Defaults-Testing.xcconfig └── Defaults.xcconfig ├── Docs.md ├── Example ├── InterposeExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── InterposeExample.xcscheme ├── InterposeExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Info.plist │ ├── InterposeExample.entitlements │ ├── SceneDelegate.swift │ └── ViewController.swift └── InterposeExampleTests │ ├── Info.plist │ └── InterposeExampleTests.swift ├── Gemfile ├── Gemfile.lock ├── InterposeKit.podspec ├── InterposeKit.xcodeproj ├── Info-Tests.plist ├── Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── InterposeKit.xcscheme │ └── InterposeTests.xcscheme ├── InterposeKit.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── InterposeTestHost ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── InterposeTestHost.entitlements └── ViewController.swift ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── InterposeKit │ ├── AnyHook.swift │ ├── ClassHook.swift │ ├── HookFinder.swift │ ├── InterposeError.swift │ ├── InterposeKit.h │ ├── InterposeKit.swift │ ├── InterposeSubclass.swift │ ├── LinuxCompileSupport.swift │ ├── ObjectHook.swift │ └── Watcher.swift └── SuperBuilder │ ├── include │ └── ITKSuperBuilder.h │ └── src │ └── ITKSuperBuilder.m ├── Tests ├── InterposeKitTests │ ├── InterposeKitTestCase.swift │ ├── InterposeKitTests.swift │ ├── KVOTests.swift │ ├── MultipleInterposing.swift │ ├── ObjectInterposeTests.swift │ ├── TestClass.swift │ └── XCTestManifests.swift └── LinuxMain.swift ├── logo-social.png └── logo.png /.codecov.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | - Sources/InterposeKit -------------------------------------------------------------------------------- /.github/workflows/SwiftPM.yml: -------------------------------------------------------------------------------- 1 | name: SwiftPM 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_11.5.app/Contents/Developer 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: swift build -v 21 | - name: Run tests 22 | run: swift test -v 23 | -------------------------------------------------------------------------------- /.github/workflows/jazzy.yml: -------------------------------------------------------------------------------- 1 | name: Jazzy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '.github/workflows/jazzy.yml' 8 | - '.jazzy.yaml' 9 | - '**/*.md' 10 | - '**/*.jpg' 11 | - 'Gemfile*' 12 | - 'Package*' 13 | - 'Sources/**/*.swift' 14 | pull_request: 15 | paths: 16 | - '.github/workflows/jazzy.yml' 17 | - '.jazzy.yaml' 18 | - '**/*.md' 19 | - '**/*.jpg' 20 | - 'Gemfile*' 21 | - 'Package*' 22 | - 'Sources/**/*.swift' 23 | 24 | jobs: 25 | Jazzy: 26 | runs-on: ubuntu-latest 27 | container: 28 | image: norionomura/jazzy:0.13.3_swift-5.2.1 29 | steps: 30 | - uses: actions/checkout@v2 31 | - run: bundle install --path vendor/bundle 32 | - run: swift build 33 | - name: Generate documentation json 34 | run: sourcekitten doc --spm-module InterposeKit > interposekit.json 35 | - name: Run jazzy 36 | run: bundle exec jazzy --clean --sourcekitten-sourcefile interposekit.json 37 | - name: Validate documentation coverage 38 | run: | 39 | if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then 40 | echo "Undocumented declarations:" 41 | cat docs/undocumented.json 42 | exit 1 43 | fi 44 | - name: Upload Artifact 45 | uses: actions/upload-artifact@v1 46 | with: 47 | name: API Docs 48 | path: docs 49 | - name: Push to gh-pages 50 | if: github.event_name == 'push' 51 | run: | 52 | git config --global user.email "${GITHUB_ACTOR}" 53 | git config --global user.name "${GITHUB_ACTOR}@users.noreply.github.com" 54 | git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" out 55 | 56 | cd out 57 | git checkout gh-pages 58 | git rm -rf . 59 | cd .. 60 | 61 | cp -a docs/. out/. 62 | cd out 63 | echo "interposekit.com " > CNAME 64 | 65 | git add -A 66 | git commit -m "Automated deployment to GitHub Pages: ${GITHUB_SHA}" --allow-empty 67 | 68 | git push origin gh-pages 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/pod_lib_lint.yml: -------------------------------------------------------------------------------- 1 | name: pod lib lint 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '.github/workflows/pod_lib_lint.yml' 8 | - '*.podspec' 9 | - 'Gemfile*' 10 | - 'Sources/**/*.[ch]' 11 | - 'Sources/**/*.swift' 12 | pull_request: 13 | paths: 14 | - '.github/workflows/pod_lib_lint.yml' 15 | - '*.podspec' 16 | - 'Gemfile*' 17 | - 'Sources/**/*.[ch]' 18 | - 'Sources/**/*.swift' 19 | 20 | jobs: 21 | pod_lib_lint: 22 | name: pod lib lint 23 | runs-on: macos-latest 24 | env: 25 | DEVELOPER_DIR: /Applications/Xcode_11.5.app 26 | steps: 27 | - uses: actions/checkout@v2 28 | - run: bundle install --path vendor/bundle 29 | - run: bundle exec pod lib lint --verbose -------------------------------------------------------------------------------- /.github/workflows/swiftlint.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.github/workflows/swiftlint.yml' 7 | - '.swiftlint.yml' 8 | - '**/*.swift' 9 | 10 | jobs: 11 | SwiftLint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: GitHub Action for SwiftLint 16 | uses: norio-nomura/action-swiftlint@3.1.0 -------------------------------------------------------------------------------- /.github/workflows/swiftlint_analyze.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLint Analyze 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '.github/workflows/swiftlint_analyze.yml' 8 | - 'InterposeKit.xcodeproj/**' 9 | - 'Sources/**/*.[ch]' 10 | - 'Sources/**/*.swift' 11 | - '!Tests/**/*.swift' 12 | - '!Tests/LinuxMain.swift' 13 | pull_request: 14 | paths: 15 | - '.github/workflows/swiftlint_analyze.yml' 16 | - 'InterposeKit.xcodeproj/**' 17 | - 'Sources/**/*.[ch]' 18 | - 'Sources/**/*.swift' 19 | - '!Tests/**/*.swift' 20 | - '!Tests/LinuxMain.swift' 21 | 22 | jobs: 23 | Analyze: 24 | runs-on: macos-latest 25 | env: 26 | DEVELOPER_DIR: /Applications/Xcode_11.5.app 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Generate xcodebuild.log 30 | run: xcodebuild -scheme InterposeKit -project InterposeKit.xcodeproj clean build-for-testing > xcodebuild.log 31 | shell: bash 32 | - name: Install SwiftLint 33 | run: HOMEBREW_NO_AUTO_UPDATE=1 brew install https://raw.github.com/Homebrew/homebrew-core/master/Formula/swiftlint.rb 34 | - name: Run SwiftLint Analyze 35 | run: swiftlint analyze --strict --compiler-log-path xcodebuild.log --reporter github-actions-logging 36 | -------------------------------------------------------------------------------- /.github/workflows/xcodebuild.yml: -------------------------------------------------------------------------------- 1 | name: xcodebuild 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - '.github/workflows/xcodebuild.yml' 8 | - 'InterposeKit.xcodeproj/**' 9 | - 'Sources/**/*.[ch]' 10 | - 'Sources/**/*.swift' 11 | - 'Tests/**/*.swift' 12 | - '!Tests/LinuxMain.swift' 13 | pull_request: 14 | paths: 15 | - '.github/workflows/xcodebuild.yml' 16 | - 'InterposeKit.xcodeproj/**' 17 | - 'Sources/**/*.[ch]' 18 | - 'Sources/**/*.swift' 19 | - 'Tests/**/*.swift' 20 | - '!Tests/LinuxMain.swift' 21 | 22 | jobs: 23 | xcodebuild: 24 | strategy: 25 | matrix: 26 | xcode: 27 | - version: '11.4' 28 | flags_for_test: -parallel-testing-enabled NO -enableCodeCoverage YES 29 | - version: '11.5' 30 | flags_for_test: -parallel-testing-enabled NO -enableCodeCoverage YES 31 | xcode_flags: ['-scheme InterposeKit -project InterposeKit.xcodeproj'] 32 | runs-on: macos-latest 33 | env: 34 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode.version }}.app 35 | steps: 36 | - uses: actions/checkout@v2 37 | - run: xcodebuild -version 38 | - name: macOS with UTF16 39 | if: always() 40 | run: YAMS_DEFAULT_ENCODING=UTF16 xcodebuild ${{ matrix.xcode_flags }} ${{ matrix.xcode.flags_for_test }} test | xcpretty 41 | shell: bash 42 | - name: macOS with UTF8 43 | if: always() 44 | run: YAMS_DEFAULT_ENCODING=UTF8 xcodebuild ${{ matrix.xcode_flags }} ${{ matrix.xcode.flags_for_test }} test | xcpretty 45 | shell: bash 46 | - name: iPhone Simulator 47 | if: always() 48 | run: xcodebuild ${{ matrix.xcode_flags }} ${{ matrix.xcode.flags_for_test }} test -sdk iphonesimulator -destination "name=iPhone 8" | xcpretty 49 | shell: bash 50 | - name: Apple TV Simulator 51 | if: always() 52 | run: xcodebuild ${{ matrix.xcode_flags }} ${{ matrix.xcode.flags_for_test }} test -sdk appletvsimulator -destination "name=Apple TV 4K" | xcpretty 53 | shell: bash 54 | - name: watchOS Simulator 55 | if: always() 56 | run: xcodebuild ${{ matrix.xcode_flags }} build -sdk watchsimulator | xcpretty 57 | shell: bash 58 | - name: Codecov 59 | if: contains(matrix.xcode.flags_for_test, '-enableCodeCoverage YES') 60 | run: | 61 | if [[ -n "${CODECOV_TOKEN}" ]]; then 62 | curl -s https://codecov.io/bash | bash -s 63 | fi 64 | shell: bash 65 | env: { 'CODECOV_TOKEN': '${{ secrets.CODECOV_TOKEN }}' } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | 92 | .DS_Store 93 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | module: InterposeKit 2 | author: Peter Steinberger 3 | author_url: https://steipete.com 4 | root_url: https://pages.github.com/steipete/InterposeKit 5 | github_url: https://github.com/steipete/InterposeKit 6 | github_file_prefix: https://github.com/steipete/InterposeKit/tree/master 7 | theme: fullwidth 8 | clean: true 9 | copyright: '© 2020 [Peter Steinberger](https://steipete.com) under MIT.' -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | - Tests 4 | analyzer_rules: 5 | disallowed_racist_terms_of_art: 6 | name: "Disallowed racist terms of art" 7 | regex: 'blacklist|whitelist|^((? 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Master 2 | 3 | ##### Breaking 4 | 5 | * None. 6 | 7 | ##### Enhancements 8 | 9 | * None. 10 | 11 | ##### Bug Fixes 12 | 13 | * None. 14 | 15 | ## 0.01 16 | 17 | ##### Breaking 18 | 19 | * Swift 5.2 or later is required to build InterposeKit. 20 | [Peter Steinberger](https://github.com/steipete) 21 | 22 | ##### Enhancements 23 | 24 | * Initial Release. 25 | 26 | ##### Bug Fixes 27 | 28 | * None. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Tracking Changes 2 | 3 | All changes should be made via pull requests on GitHub. 4 | 5 | When issuing a pull request, please add a summary of your changes to the 6 | `CHANGELOG.md` file. 7 | 8 | We follow the same syntax as [CocoaPods' `CHANGELOG.md`](https://github.com/CocoaPods/CocoaPods/blob/master/CHANGELOG.md): 9 | 10 | 1. One Markdown unnumbered list item describing the change. 11 | 2. 2 trailing spaces on the last line describing the change. 12 | 3. A list of Markdown hyperlinks to the change's contributors. One entry 13 | per line. Usually just one. 14 | 4. A list of Markdown hyperlinks to the issues the change addresses. One entry 15 | per line. Usually just one. 16 | 5. All `CHANGELOG.md` content is hard-wrapped at 80 characters. 17 | 18 | By submitting a pull request, you represent that you have the right to license 19 | your contribution to Peter Steinberger and the community, and agree by submitting the patch that your contributions are licensed under the InterposeKit project license. 20 | 21 | Before submitting the pull request, please make sure you have tested your changes. -------------------------------------------------------------------------------- /Configuration/Defaults-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults-Debug.xcconfig 3 | // 4 | // Additional defaults for debugging. 5 | // 6 | 7 | #include "Defaults.xcconfig" 8 | 9 | // Set the debugging flag 10 | GCC_PREPROCESSOR_DEFINITIONS = $(PSPDF_PREPROCESSOR_DEFINITIONS_COMMON) DEBUG=1 11 | -------------------------------------------------------------------------------- /Configuration/Defaults-Release.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults-Release.xcconfig 3 | // 4 | // Additional defaults to have a unified set of optimization settings 5 | // 6 | 7 | #include "Defaults.xcconfig" 8 | 9 | // Performance 10 | GCC_UNROLL_LOOPS = YES 11 | GCC_OPTIMIZATION_LEVEL = s 12 | 13 | // Without this set to yes, xcode only passes -fembed-bitcode-marker, not -fembed-bitcode to the compiler, when ENABLE_BITCODE is Yes 14 | DEPLOYMENT_POSTPROCESSING = YES 15 | 16 | // Enable bitcode is YES by default on iOS. Mac doesn't support it, so setting that flag would break Mac. 17 | ENABLE_BITCODE = NO 18 | ENABLE_BITCODE[sdk=iphoneos*] = YES 19 | 20 | // Link-Time optimization reduces file size by quite a bit. 21 | LLVM_LTO = YES 22 | 23 | // Code protection 24 | STRIP_INSTALLED_PRODUCT = YES 25 | SEPARATE_STRIP = YES 26 | COPY_PHASE_STRIP = YES 27 | DEAD_CODE_STRIPPING = YES 28 | STRIP_STYLE = non-global 29 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 30 | 31 | // Faster assset compilation for debug mode 32 | ASSETCATALOG_COMPILER_OPTIMIZATION = time; 33 | 34 | // build all the architectures 35 | ONLY_ACTIVE_ARCH = NO 36 | ENABLE_TESTABILITY = NO 37 | VALIDATE_PRODUCT = YES 38 | 39 | // See Defaults.xcconfig - we always want assertions to be enabled, also for release builds. 40 | //ENABLE_NS_ASSERTIONS = NO 41 | 42 | // Generating dSYM files is super slow but necessary for release builds. 43 | // This simply restores the default that we override in Defaults.xcconfig. 44 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym; 45 | 46 | // deep static analysis before building 47 | //RUN_CLANG_STATIC_ANALYZER = YES 48 | //CLANG_STATIC_ANALYZER_MODE = deep 49 | 50 | // Warning are errors! 51 | GCC_TREAT_WARNINGS_AS_ERRORS = NO 52 | SWIFT_TREAT_WARNINGS_AS_ERRORS = NO 53 | 54 | // Swift 55 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE 56 | SWIFT_OPTIMIZATION_LEVEL = -O 57 | SWIFT_COMPILATION_MODE = wholemodule; 58 | 59 | // Disable SwiftLint 60 | PSPDF_SWIFTLINT = NO 61 | 62 | // -gline-tables-only reduces debug information slightly but speeds up LTO. 63 | // Debug info for variables or function parameters is not produced, which reduces the size of the resulting binary. 64 | // http://llvm.org/releases/3.2/tools/clang/docs/ReleaseNotes.html 65 | // This has been added in Xcode 8. 66 | OTHER_CFLAGS = $(PSPDF_CFLAGS_COMMON) -gline-tables-only -fembed-bitcode 67 | OTHER_CFLAGS[sdk=macosx*] = $(PSPDF_CFLAGS_COMMON) -gline-tables-only 68 | 69 | // ** DEBUGGING ONLY ** 70 | //OTHER_CFLAGS = $(PSPDF_CFLAGS_COMMON) -v 71 | //OTHER_LDFLAGS = -ObjC -v 72 | //OTHER_LIBTOOLFLAGS = -v 73 | //WARNING_LDFLAGS = -v 74 | -------------------------------------------------------------------------------- /Configuration/Defaults-Testing.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults-Testing.xcconfig 3 | // 4 | // Additional defaults that are used for testing. 5 | // 6 | 7 | #include "Defaults.xcconfig" 8 | 9 | // Define the TESTING macro in all debug builds 10 | GCC_PREPROCESSOR_DEFINITIONS = $(PSPDF_PREPROCESSOR_DEFINITIONS_COMMON) CI=$(CI) DEBUG=1 TESTING=1 11 | 12 | // Explicitely enable assertions, just to be sure. 13 | ENABLE_NS_ASSERTIONS = YES 14 | 15 | // Enable extra validation 16 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html 17 | VALIDATE_PRODUCT = YES 18 | 19 | // Default value for CI 20 | CI = 0 21 | -------------------------------------------------------------------------------- /Configuration/Defaults.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Defaults.xcconfig 3 | // 4 | // Common customizations/sanitizations of the platform defaults for our projects. 5 | // 6 | // For compatibility reasons, the iOS platform has a couple of not–so–sensible defaults. 7 | // This file collects, and annotates the deviations from these defaults. 8 | // 9 | // :NOTE: Some of the settings herein may be redundant for newly created projects/targets: 10 | // Xcode’s project/target templates take several (but not all!) of these issues into account. 11 | // However, for uniformity it is probably more useful to simply delete the entire warning section in the Xcode project, and then set them up using the standard warning flags. 12 | // https://developer.apple.com/library/mac/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html 13 | 14 | // We support macOS, iOS, watchOS and tvOS by default. 15 | SUPPORTED_PLATFORMS = macosx iphoneos appletvos watchos appletvsimulator iphonesimulator watchsimulator 16 | 17 | // http://promisekit.org/news/2016/08/Multiplatform-Single-Scheme-Xcode-Projects/ 18 | // watchOS build fails if we do not set it exactly like that 19 | TARGETED_DEVICE_FAMILY = 1,2,3,4 20 | 21 | // Deployment targets 22 | MACOSX_DEPLOYMENT_TARGET = 10.13 23 | IPHONEOS_DEPLOYMENT_TARGET = 12.0 24 | TVOS_DEPLOYMENT_TARGET = 11.0 25 | WATCHOS_DEPLOYMENT_TARGET = 5.0 26 | 27 | // We do not need to codesign tests for the simulator - disabling speeds up test compile (sign) time. 28 | CODE_SIGN_IDENTITY = iOS Developer 29 | CODE_SIGN_IDENTITY[sdk=iphoneos*] = 30 | CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = 31 | CODE_SIGN_IDENTITY[sdk=macosx*] = 32 | 33 | // :MARK: General Project Setup: 34 | // :MARK: -Toolchain 35 | // Universal Objective-C iOS Project 36 | SDKROOT = iphoneos 37 | 38 | LLVM_LTO = NO 39 | 40 | // Performance 41 | ENABLE_TESTABILITY = YES 42 | VALIDATE_PRODUCT = NO 43 | COPY_PHASE_STRIP = NO 44 | DEPLOYMENT_POSTPROCESSING = NO 45 | STRIP_INSTALLED_PRODUCT = NO 46 | SEPARATE_STRIP = NO 47 | DEAD_CODE_STRIPPING = NO 48 | STRIP_STYLE = debugging 49 | 50 | // Limit API to what is safe for extensions. 51 | APPLICATION_EXTENSION_API_ONLY = YES 52 | 53 | // Everything else kills debugging and performance. 54 | GCC_OPTIMIZATION_LEVEL = 0 55 | 56 | // TODO: Neither sdk=uikitformac* nor sdk=maccatalyst nor sdk=iosmac work 57 | // xcodebuild -showsdks doesn't list macCatalyst either 58 | // https://twitter.com/owensd/status/1154472549440299009 59 | // FB6822740: Mac Catalyst: Please add official support for a separate architecture in xcconfig files [sdk=maccatalyst] 60 | MACCATALYST_YES = YES 61 | MACCATALYST_NO = NO 62 | // This will be the case on iOS 63 | MACCATALYST_ = NO 64 | 65 | // This is always defined as YES or NO 66 | PSPDF_IS_MACCATALYST = $(MACCATALYST_$(IS_MACCATALYST)) 67 | 68 | // macOS specific: We always want to use the hardened runtime. 69 | // This is a requirement for the Mac App Store. 70 | ENABLE_HARDENED_RUNTIME[sdk=macosx*] = $(PSPDF_IS_MACCATALYST) 71 | 72 | // :MARK: -Swift 73 | SWIFT_VERSION = 5 74 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG 75 | 76 | // Same reason as GCC_OPTIMIZATION_LEVEL 77 | SWIFT_OPTIMIZATION_LEVEL = -Onone 78 | 79 | // Warn on unguarded API usage 80 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 81 | 82 | // :MARK: - Reset Diagnostics: 83 | ENABLE_STRICT_OBJC_MSGSEND = YES 84 | // :MARK: -Warnings 85 | // The iOS platform defaults **explicitly** disable several warnings. 86 | // This defeats the purpose of Clangs `-Weverything`, which we want to use! 87 | // Therefore, we explicitly enable all those warnings here. 88 | // Disabling those we do **not** want then happens elsewhere — always in combination with a reason _why_ we don’t want/need it. 89 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 90 | CLANG_WARN_EMPTY_BODY = YES 91 | GCC_WARN_SHADOW = YES 92 | CLANG_WARN_CONSTANT_CONVERSION = YES 93 | CLANG_WARN_BOOL_CONVERSION = YES 94 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 95 | CLANG_WARN_ENUM_CONVERSION = YES 96 | CLANG_WARN_INT_CONVERSION = YES 97 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES 98 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES 99 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES 100 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES 101 | CLANG_WARN_ASSIGN_ENUM = YES 102 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES 103 | GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES 104 | GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES 105 | GCC_WARN_UNKNOWN_PRAGMAS = YES 106 | CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE 107 | GCC_WARN_UNUSED_FUNCTION = YES 108 | GCC_WARN_UNUSED_LABEL = YES 109 | GCC_WARN_UNUSED_PARAMETER = YES 110 | GCC_WARN_UNUSED_VARIABLE = YES 111 | CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES 112 | GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES 113 | GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES 114 | CLANG_WARN_CXX0X_EXTENSIONS = YES 115 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 116 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 117 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES 118 | CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES 119 | GCC_WARN_UNDECLARED_SELECTOR = YES 120 | GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES 121 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 122 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 123 | GCC_WARN_UNDECLARED_SELECTOR = YES 124 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 125 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES 126 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 127 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 128 | CLANG_WARN_OBJC_RECEIVER_WEAK = YES 129 | CLANG_WARN_INFINITE_RECURSION = YES 130 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES_ERROR 131 | CLANG_WARN_COMMA = YES_ERROR 132 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 133 | CLANG_WARN_STRICT_PROTOTYPES = YES_ERROR 134 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES 135 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES_ERROR 136 | 137 | // Whether to warn when the value returned from a function/method/block does not 138 | // match its return type 139 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR 140 | 141 | // Warn if a variable might be clobbered by a setjmp call or if an automatic variable is used without prior initialization. 142 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 143 | 144 | // Whether to warn about unsafe comparisons between values of different 145 | // signedness 146 | GCC_WARN_SIGN_COMPARE = YES 147 | 148 | // Whether to warn about the arguments to printf-style functions not matching 149 | // the format specifiers 150 | GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES 151 | 152 | // Whether to warn about missing braces or parentheses that make the meaning of 153 | // the code ambiguous 154 | GCC_WARN_MISSING_PARENTHESES = YES 155 | 156 | // Whether to warn about an aggregate data type's initializer not being fully 157 | // bracketed (e.g., array initializer syntax) 158 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES 159 | 160 | // This warning detects suspicious uses of std::move. 161 | CLANG_WARN_SUSPICIOUS_MOVE = YES 162 | 163 | // Whether to warn about the use of four-character constants 164 | GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES 165 | 166 | // Whether to warn when switching on an enum value, and all possibilities are 167 | // not accounted for 168 | GCC_WARN_CHECK_SWITCH_STATEMENTS = YES 169 | 170 | // Warn for usage of implicit sequentially-consistent atomics 171 | CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES 172 | 173 | // Warning are not errors by default 174 | GCC_TREAT_WARNINGS_AS_ERRORS = NO 175 | SWIFT_TREAT_WARNINGS_AS_ERRORS = NO 176 | 177 | // Xcode 11: Clang now provides a mechanism for controlling exit-time destructor registration. You can disable these globally with the flag -fno-c++-static-destructors, or apply the attribute [[clang::no_destroy]] to disable the destructors of specific variables. The attribute [[clang::always_destroy]] was also added to enable destructors of specific variables when -fno-c++-static-destructors is used. (21734598) 178 | // We do not need to destruct statics on iOS at all, so this is a space optimization. 179 | CLANG_ENABLE_CPP_STATIC_DESTRUCTORS = NO 180 | 181 | // :MARK: -Static Analysis 182 | // As with the warnings, reset all options that are explicitly disabled by the iOS platform defaults 183 | CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES 184 | CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES 185 | CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES 186 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE 187 | 188 | // http://www.miqu.me/blog/2016/07/31/xcode-8-new-build-settings-and-analyzer-improvements/ 189 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES 190 | CLANG_ANALYZER_NONNULL = YES 191 | CLANG_ANALYZER_OBJC_DEALLOC = YES 192 | LOCALIZED_STRING_MACRO_NAMES = $(inherited) PSPDFLocalize 193 | 194 | // Check for C++ container overflow when Address Sanitizer is enabled. 195 | CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; 196 | 197 | // Configure undefined behavior checker. 198 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; 199 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES; 200 | 201 | // Warn about assigning integer constants to enum values that are out of the range of the enumerated type. 202 | CLANG_WARN_ASSIGN_ENUM = YES 203 | 204 | // Warns when a quoted include is used instead of a framework style include in a framework header. 205 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES 206 | 207 | // Warn for unnecessary semicolons 208 | CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES 209 | 210 | // Warn when a source file does not end with a newline. 211 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES 212 | 213 | // Check for Grand Central Dispatch idioms that may lead to poor performance. 214 | CLANG_ANALYZER_GCD_PERFORMANCE = NO 215 | 216 | // Warn about implicit ownership types on Objective-C object references as out parameters. For example, declaring a parameter with type `NSObject**` will produce a warning because the compiler will assume that the out parameter's ownership type is `__autoreleasing` 217 | CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES 218 | 219 | CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES 220 | 221 | // Whether to warn about implicit conversions in the signedness of the type 222 | // a pointer is pointing to (e.g., 'int *' getting converted to 'unsigned int *') 223 | GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES 224 | -------------------------------------------------------------------------------- /Docs.md: -------------------------------------------------------------------------------- 1 | # InterposeKit Documentation 2 | 3 | For installation instructions, see [README.md](README.md). 4 | 5 | API documentation available at [http://interposekit.com](http://interposekit.com/) 6 | -------------------------------------------------------------------------------- /Example/InterposeExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */; }; 11 | 7880B124248280B300AD2251 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B123248280B300AD2251 /* AppDelegate.swift */; }; 12 | 7880B126248280B300AD2251 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B125248280B300AD2251 /* SceneDelegate.swift */; }; 13 | 7880B128248280B300AD2251 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7880B127248280B300AD2251 /* ViewController.swift */; }; 14 | 7880B12B248280B300AD2251 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7880B129248280B300AD2251 /* Main.storyboard */; }; 15 | 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12C248280B500AD2251 /* Assets.xcassets */; }; 16 | 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7880B12E248280B500AD2251 /* LaunchScreen.storyboard */; }; 17 | 78C39DDC2483363300B46395 /* InterposeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 78C39DDB2483363300B46395 /* InterposeKit */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 7880B137248280B500AD2251 /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 7880B118248280B300AD2251 /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 7880B11F248280B300AD2251; 26 | remoteInfo = InterposeExample; 27 | }; 28 | /* End PBXContainerItemProxy section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InterposeExampleTests.swift; sourceTree = ""; }; 32 | 781095BE248D8AD7008A943C /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 33 | 7880B120248280B300AD2251 /* InterposeExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InterposeExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 7880B123248280B300AD2251 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | 7880B125248280B300AD2251 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 36 | 7880B127248280B300AD2251 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 37 | 7880B12A248280B300AD2251 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 7880B12C248280B500AD2251 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | 7880B12F248280B500AD2251 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 7880B131248280B500AD2251 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 7880B136248280B500AD2251 /* InterposeExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InterposeExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | 7880B14B248280D500AD2251 /* InterposeExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = InterposeExample.entitlements; sourceTree = ""; }; 43 | 78C39DD8248335B100B46395 /* Interpose */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Interpose; path = ..; sourceTree = ""; }; 44 | 78C39DDE2483366B00B46395 /* Defaults-Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Defaults-Release.xcconfig"; sourceTree = ""; }; 45 | 78C39DDF2483366B00B46395 /* Defaults-Testing.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Defaults-Testing.xcconfig"; sourceTree = ""; }; 46 | 78C39DE02483366B00B46395 /* Defaults.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Defaults.xcconfig; sourceTree = ""; }; 47 | 78C39DE12483366B00B46395 /* Defaults-Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Defaults-Debug.xcconfig"; sourceTree = ""; }; 48 | /* End PBXFileReference section */ 49 | 50 | /* Begin PBXFrameworksBuildPhase section */ 51 | 7880B11D248280B300AD2251 /* Frameworks */ = { 52 | isa = PBXFrameworksBuildPhase; 53 | buildActionMask = 2147483647; 54 | files = ( 55 | 78C39DDC2483363300B46395 /* InterposeKit in Frameworks */, 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | 7880B133248280B500AD2251 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | ); 64 | runOnlyForDeploymentPostprocessing = 0; 65 | }; 66 | /* End PBXFrameworksBuildPhase section */ 67 | 68 | /* Begin PBXGroup section */ 69 | 781095BC248D8AD7008A943C /* InterposeExampleTests */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 781095BD248D8AD7008A943C /* InterposeExampleTests.swift */, 73 | 781095BE248D8AD7008A943C /* Info.plist */, 74 | ); 75 | path = InterposeExampleTests; 76 | sourceTree = ""; 77 | }; 78 | 7880B117248280B300AD2251 = { 79 | isa = PBXGroup; 80 | children = ( 81 | 78C39DD8248335B100B46395 /* Interpose */, 82 | 7880B122248280B300AD2251 /* InterposeExample */, 83 | 781095BC248D8AD7008A943C /* InterposeExampleTests */, 84 | 7880B121248280B300AD2251 /* Products */, 85 | 7880B14E248281D000AD2251 /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 7880B121248280B300AD2251 /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 7880B120248280B300AD2251 /* InterposeExample.app */, 93 | 7880B136248280B500AD2251 /* InterposeExampleTests.xctest */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 7880B122248280B300AD2251 /* InterposeExample */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 78C39DDD2483366B00B46395 /* Configuration */, 102 | 7880B14B248280D500AD2251 /* InterposeExample.entitlements */, 103 | 7880B123248280B300AD2251 /* AppDelegate.swift */, 104 | 7880B125248280B300AD2251 /* SceneDelegate.swift */, 105 | 7880B127248280B300AD2251 /* ViewController.swift */, 106 | 7880B129248280B300AD2251 /* Main.storyboard */, 107 | 7880B12C248280B500AD2251 /* Assets.xcassets */, 108 | 7880B12E248280B500AD2251 /* LaunchScreen.storyboard */, 109 | 7880B131248280B500AD2251 /* Info.plist */, 110 | ); 111 | path = InterposeExample; 112 | sourceTree = ""; 113 | }; 114 | 7880B14E248281D000AD2251 /* Frameworks */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = Frameworks; 119 | sourceTree = ""; 120 | }; 121 | 78C39DDD2483366B00B46395 /* Configuration */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 78C39DDE2483366B00B46395 /* Defaults-Release.xcconfig */, 125 | 78C39DDF2483366B00B46395 /* Defaults-Testing.xcconfig */, 126 | 78C39DE02483366B00B46395 /* Defaults.xcconfig */, 127 | 78C39DE12483366B00B46395 /* Defaults-Debug.xcconfig */, 128 | ); 129 | name = Configuration; 130 | path = ../../Configuration; 131 | sourceTree = ""; 132 | }; 133 | /* End PBXGroup section */ 134 | 135 | /* Begin PBXNativeTarget section */ 136 | 7880B11F248280B300AD2251 /* InterposeExample */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 7880B13F248280B500AD2251 /* Build configuration list for PBXNativeTarget "InterposeExample" */; 139 | buildPhases = ( 140 | 7880B11C248280B300AD2251 /* Sources */, 141 | 7880B11D248280B300AD2251 /* Frameworks */, 142 | 7880B11E248280B300AD2251 /* Resources */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | 78C39DDA2483362F00B46395 /* PBXTargetDependency */, 148 | ); 149 | name = InterposeExample; 150 | packageProductDependencies = ( 151 | 78C39DDB2483363300B46395 /* InterposeKit */, 152 | ); 153 | productName = InterposeExample; 154 | productReference = 7880B120248280B300AD2251 /* InterposeExample.app */; 155 | productType = "com.apple.product-type.application"; 156 | }; 157 | 7880B135248280B500AD2251 /* InterposeExampleTests */ = { 158 | isa = PBXNativeTarget; 159 | buildConfigurationList = 7880B142248280B500AD2251 /* Build configuration list for PBXNativeTarget "InterposeExampleTests" */; 160 | buildPhases = ( 161 | 7880B132248280B500AD2251 /* Sources */, 162 | 7880B133248280B500AD2251 /* Frameworks */, 163 | 7880B134248280B500AD2251 /* Resources */, 164 | ); 165 | buildRules = ( 166 | ); 167 | dependencies = ( 168 | 7880B138248280B500AD2251 /* PBXTargetDependency */, 169 | ); 170 | name = InterposeExampleTests; 171 | productName = InterposeExampleTests; 172 | productReference = 7880B136248280B500AD2251 /* InterposeExampleTests.xctest */; 173 | productType = "com.apple.product-type.bundle.unit-test"; 174 | }; 175 | /* End PBXNativeTarget section */ 176 | 177 | /* Begin PBXProject section */ 178 | 7880B118248280B300AD2251 /* Project object */ = { 179 | isa = PBXProject; 180 | attributes = { 181 | LastSwiftUpdateCheck = 1150; 182 | LastUpgradeCheck = 1160; 183 | ORGANIZATIONNAME = "PSPDFKit GmbH"; 184 | TargetAttributes = { 185 | 7880B11F248280B300AD2251 = { 186 | CreatedOnToolsVersion = 11.5; 187 | }; 188 | 7880B135248280B500AD2251 = { 189 | CreatedOnToolsVersion = 11.5; 190 | TestTargetID = 7880B11F248280B300AD2251; 191 | }; 192 | }; 193 | }; 194 | buildConfigurationList = 7880B11B248280B300AD2251 /* Build configuration list for PBXProject "InterposeExample" */; 195 | compatibilityVersion = "Xcode 9.3"; 196 | developmentRegion = en; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | Base, 201 | ); 202 | mainGroup = 7880B117248280B300AD2251; 203 | packageReferences = ( 204 | ); 205 | productRefGroup = 7880B121248280B300AD2251 /* Products */; 206 | projectDirPath = ""; 207 | projectRoot = ""; 208 | targets = ( 209 | 7880B11F248280B300AD2251 /* InterposeExample */, 210 | 7880B135248280B500AD2251 /* InterposeExampleTests */, 211 | ); 212 | }; 213 | /* End PBXProject section */ 214 | 215 | /* Begin PBXResourcesBuildPhase section */ 216 | 7880B11E248280B300AD2251 /* Resources */ = { 217 | isa = PBXResourcesBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 7880B130248280B500AD2251 /* LaunchScreen.storyboard in Resources */, 221 | 7880B12D248280B500AD2251 /* Assets.xcassets in Resources */, 222 | 7880B12B248280B300AD2251 /* Main.storyboard in Resources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | 7880B134248280B500AD2251 /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXResourcesBuildPhase section */ 234 | 235 | /* Begin PBXSourcesBuildPhase section */ 236 | 7880B11C248280B300AD2251 /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 7880B128248280B300AD2251 /* ViewController.swift in Sources */, 241 | 7880B124248280B300AD2251 /* AppDelegate.swift in Sources */, 242 | 7880B126248280B300AD2251 /* SceneDelegate.swift in Sources */, 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | }; 246 | 7880B132248280B500AD2251 /* Sources */ = { 247 | isa = PBXSourcesBuildPhase; 248 | buildActionMask = 2147483647; 249 | files = ( 250 | 781095BF248D8AD7008A943C /* InterposeExampleTests.swift in Sources */, 251 | ); 252 | runOnlyForDeploymentPostprocessing = 0; 253 | }; 254 | /* End PBXSourcesBuildPhase section */ 255 | 256 | /* Begin PBXTargetDependency section */ 257 | 7880B138248280B500AD2251 /* PBXTargetDependency */ = { 258 | isa = PBXTargetDependency; 259 | target = 7880B11F248280B300AD2251 /* InterposeExample */; 260 | targetProxy = 7880B137248280B500AD2251 /* PBXContainerItemProxy */; 261 | }; 262 | 78C39DDA2483362F00B46395 /* PBXTargetDependency */ = { 263 | isa = PBXTargetDependency; 264 | productRef = 78C39DD92483362F00B46395 /* InterposeKit */; 265 | }; 266 | /* End PBXTargetDependency section */ 267 | 268 | /* Begin PBXVariantGroup section */ 269 | 7880B129248280B300AD2251 /* Main.storyboard */ = { 270 | isa = PBXVariantGroup; 271 | children = ( 272 | 7880B12A248280B300AD2251 /* Base */, 273 | ); 274 | name = Main.storyboard; 275 | sourceTree = ""; 276 | }; 277 | 7880B12E248280B500AD2251 /* LaunchScreen.storyboard */ = { 278 | isa = PBXVariantGroup; 279 | children = ( 280 | 7880B12F248280B500AD2251 /* Base */, 281 | ); 282 | name = LaunchScreen.storyboard; 283 | sourceTree = ""; 284 | }; 285 | /* End PBXVariantGroup section */ 286 | 287 | /* Begin XCBuildConfiguration section */ 288 | 7880B13D248280B500AD2251 /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | baseConfigurationReference = 78C39DE12483366B00B46395 /* Defaults-Debug.xcconfig */; 291 | buildSettings = { 292 | ALWAYS_SEARCH_USER_PATHS = NO; 293 | CLANG_ANALYZER_NONNULL = YES; 294 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 295 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 296 | CLANG_CXX_LIBRARY = "libc++"; 297 | CLANG_ENABLE_MODULES = YES; 298 | CLANG_ENABLE_OBJC_ARC = YES; 299 | CLANG_ENABLE_OBJC_WEAK = YES; 300 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 301 | CLANG_WARN_BOOL_CONVERSION = YES; 302 | CLANG_WARN_COMMA = YES; 303 | CLANG_WARN_CONSTANT_CONVERSION = YES; 304 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 305 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 306 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 307 | CLANG_WARN_EMPTY_BODY = YES; 308 | CLANG_WARN_ENUM_CONVERSION = YES; 309 | CLANG_WARN_INFINITE_RECURSION = YES; 310 | CLANG_WARN_INT_CONVERSION = YES; 311 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 312 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 313 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 314 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 315 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 316 | CLANG_WARN_STRICT_PROTOTYPES = YES; 317 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 318 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 319 | CLANG_WARN_UNREACHABLE_CODE = YES; 320 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 321 | COPY_PHASE_STRIP = NO; 322 | DEBUG_INFORMATION_FORMAT = dwarf; 323 | ENABLE_STRICT_OBJC_MSGSEND = YES; 324 | ENABLE_TESTABILITY = YES; 325 | GCC_C_LANGUAGE_STANDARD = gnu11; 326 | GCC_DYNAMIC_NO_PIC = NO; 327 | GCC_NO_COMMON_BLOCKS = YES; 328 | GCC_OPTIMIZATION_LEVEL = 0; 329 | GCC_PREPROCESSOR_DEFINITIONS = ( 330 | "DEBUG=1", 331 | "$(inherited)", 332 | ); 333 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 334 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 335 | GCC_WARN_UNDECLARED_SELECTOR = YES; 336 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 337 | GCC_WARN_UNUSED_FUNCTION = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 340 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 341 | MTL_FAST_MATH = YES; 342 | ONLY_ACTIVE_ARCH = YES; 343 | SDKROOT = iphoneos; 344 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 345 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 346 | TARGETED_DEVICE_FAMILY = "1,2"; 347 | TVOS_DEPLOYMENT_TARGET = 13.0; 348 | }; 349 | name = Debug; 350 | }; 351 | 7880B13E248280B500AD2251 /* Release */ = { 352 | isa = XCBuildConfiguration; 353 | baseConfigurationReference = 78C39DDE2483366B00B46395 /* Defaults-Release.xcconfig */; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_ANALYZER_NONNULL = YES; 357 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 359 | CLANG_CXX_LIBRARY = "libc++"; 360 | CLANG_ENABLE_MODULES = YES; 361 | CLANG_ENABLE_OBJC_ARC = YES; 362 | CLANG_ENABLE_OBJC_WEAK = YES; 363 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 364 | CLANG_WARN_BOOL_CONVERSION = YES; 365 | CLANG_WARN_COMMA = YES; 366 | CLANG_WARN_CONSTANT_CONVERSION = YES; 367 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 368 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 369 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 370 | CLANG_WARN_EMPTY_BODY = YES; 371 | CLANG_WARN_ENUM_CONVERSION = YES; 372 | CLANG_WARN_INFINITE_RECURSION = YES; 373 | CLANG_WARN_INT_CONVERSION = YES; 374 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 375 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 376 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 378 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 379 | CLANG_WARN_STRICT_PROTOTYPES = YES; 380 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 381 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 382 | CLANG_WARN_UNREACHABLE_CODE = YES; 383 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 384 | COPY_PHASE_STRIP = NO; 385 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 386 | ENABLE_NS_ASSERTIONS = NO; 387 | ENABLE_STRICT_OBJC_MSGSEND = YES; 388 | GCC_C_LANGUAGE_STANDARD = gnu11; 389 | GCC_NO_COMMON_BLOCKS = YES; 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 397 | MTL_ENABLE_DEBUG_INFO = NO; 398 | MTL_FAST_MATH = YES; 399 | SDKROOT = iphoneos; 400 | SWIFT_COMPILATION_MODE = wholemodule; 401 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 402 | TARGETED_DEVICE_FAMILY = "1,2"; 403 | TVOS_DEPLOYMENT_TARGET = 13.0; 404 | VALIDATE_PRODUCT = YES; 405 | }; 406 | name = Release; 407 | }; 408 | 7880B140248280B500AD2251 /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 412 | CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; 413 | CODE_SIGN_IDENTITY = "Apple Development"; 414 | CODE_SIGN_STYLE = Automatic; 415 | DEVELOPMENT_TEAM = Y5PE65HELJ; 416 | INFOPLIST_FILE = InterposeExample/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = ( 418 | "$(inherited)", 419 | "@executable_path/Frameworks", 420 | ); 421 | PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeExample; 422 | PRODUCT_NAME = "$(TARGET_NAME)"; 423 | SUPPORTS_MACCATALYST = YES; 424 | SWIFT_VERSION = 5.0; 425 | }; 426 | name = Debug; 427 | }; 428 | 7880B141248280B500AD2251 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 432 | CODE_SIGN_ENTITLEMENTS = InterposeExample/InterposeExample.entitlements; 433 | CODE_SIGN_IDENTITY = "Apple Development"; 434 | CODE_SIGN_STYLE = Automatic; 435 | DEVELOPMENT_TEAM = Y5PE65HELJ; 436 | INFOPLIST_FILE = InterposeExample/Info.plist; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeExample; 442 | PRODUCT_NAME = "$(TARGET_NAME)"; 443 | SUPPORTS_MACCATALYST = YES; 444 | SWIFT_VERSION = 5.0; 445 | }; 446 | name = Release; 447 | }; 448 | 7880B143248280B500AD2251 /* Debug */ = { 449 | isa = XCBuildConfiguration; 450 | buildSettings = { 451 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 452 | BUNDLE_LOADER = "$(TEST_HOST)"; 453 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 454 | CODE_SIGN_STYLE = Automatic; 455 | DEVELOPMENT_TEAM = Y5PE65HELJ; 456 | INFOPLIST_FILE = InterposeExampleTests/Info.plist; 457 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 458 | LD_RUNPATH_SEARCH_PATHS = ( 459 | "$(inherited)", 460 | "@executable_path/Frameworks", 461 | "@loader_path/Frameworks", 462 | ); 463 | PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeExampleTests; 464 | PRODUCT_NAME = "$(TARGET_NAME)"; 465 | SWIFT_VERSION = 5.0; 466 | TARGETED_DEVICE_FAMILY = "1,2"; 467 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeExample.app/InterposeExample"; 468 | }; 469 | name = Debug; 470 | }; 471 | 7880B144248280B500AD2251 /* Release */ = { 472 | isa = XCBuildConfiguration; 473 | buildSettings = { 474 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 475 | BUNDLE_LOADER = "$(TEST_HOST)"; 476 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 477 | CODE_SIGN_STYLE = Automatic; 478 | DEVELOPMENT_TEAM = Y5PE65HELJ; 479 | INFOPLIST_FILE = InterposeExampleTests/Info.plist; 480 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 481 | LD_RUNPATH_SEARCH_PATHS = ( 482 | "$(inherited)", 483 | "@executable_path/Frameworks", 484 | "@loader_path/Frameworks", 485 | ); 486 | PRODUCT_BUNDLE_IDENTIFIER = com.steipete.InterposeExampleTests; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | SWIFT_VERSION = 5.0; 489 | TARGETED_DEVICE_FAMILY = "1,2"; 490 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/InterposeExample.app/InterposeExample"; 491 | }; 492 | name = Release; 493 | }; 494 | /* End XCBuildConfiguration section */ 495 | 496 | /* Begin XCConfigurationList section */ 497 | 7880B11B248280B300AD2251 /* Build configuration list for PBXProject "InterposeExample" */ = { 498 | isa = XCConfigurationList; 499 | buildConfigurations = ( 500 | 7880B13D248280B500AD2251 /* Debug */, 501 | 7880B13E248280B500AD2251 /* Release */, 502 | ); 503 | defaultConfigurationIsVisible = 0; 504 | defaultConfigurationName = Release; 505 | }; 506 | 7880B13F248280B500AD2251 /* Build configuration list for PBXNativeTarget "InterposeExample" */ = { 507 | isa = XCConfigurationList; 508 | buildConfigurations = ( 509 | 7880B140248280B500AD2251 /* Debug */, 510 | 7880B141248280B500AD2251 /* Release */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | 7880B142248280B500AD2251 /* Build configuration list for PBXNativeTarget "InterposeExampleTests" */ = { 516 | isa = XCConfigurationList; 517 | buildConfigurations = ( 518 | 7880B143248280B500AD2251 /* Debug */, 519 | 7880B144248280B500AD2251 /* Release */, 520 | ); 521 | defaultConfigurationIsVisible = 0; 522 | defaultConfigurationName = Release; 523 | }; 524 | /* End XCConfigurationList section */ 525 | 526 | /* Begin XCSwiftPackageProductDependency section */ 527 | 78C39DD92483362F00B46395 /* InterposeKit */ = { 528 | isa = XCSwiftPackageProductDependency; 529 | productName = InterposeKit; 530 | }; 531 | 78C39DDB2483363300B46395 /* InterposeKit */ = { 532 | isa = XCSwiftPackageProductDependency; 533 | productName = InterposeKit; 534 | }; 535 | /* End XCSwiftPackageProductDependency section */ 536 | }; 537 | rootObject = 7880B118248280B300AD2251 /* Project object */; 538 | } 539 | -------------------------------------------------------------------------------- /Example/InterposeExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/InterposeExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/InterposeExample.xcodeproj/xcshareddata/xcschemes/InterposeExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Example/InterposeExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // InterposeExample 4 | // 5 | // Copyright © 2020 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import InterposeKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | 16 | Interpose.isLoggingEnabled = true 17 | 18 | fixMacCatalystInputSystemSessionRace() 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | } 28 | 29 | /// We swizzle the `documentState` property of `RTIInputSystemSession` to make it thread safe. 30 | /// Sample crasher: https://gist.github.com/steipete/504e79558d861211a3a9ff794e09c817 31 | private func fixMacCatalystInputSystemSessionRace() { 32 | do { 33 | try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { 34 | 35 | let lock = DispatchQueue(label: "com.steipete.document-state-hack") 36 | 37 | try $0.hook("documentState") { (store: TypedHook<@convention(c) (AnyObject, Selector) -> AnyObject, @convention(block) (AnyObject) -> AnyObject>) in { `self` in 38 | lock.sync { store.original(`self`, store.selector) } 39 | } 40 | } 41 | 42 | try $0.hook("setDocumentState:") { (store: TypedHook<@convention(c) (AnyObject, Selector, AnyObject) -> Void, @convention(block) (AnyObject, AnyObject) -> Void>) in { `self`, newValue in 43 | lock.sync { store.original(`self`, store.selector, newValue) } 44 | } 45 | } 46 | } 47 | } catch { 48 | print("Failed to fix input system: \(error).") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Example/InterposeExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Example/InterposeExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/InterposeExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/InterposeExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Example/InterposeExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSApplicationCategoryType 22 | public.app-category.developer-tools 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | UISceneStoryboardFile 39 | Main 40 | 41 | 42 | 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIMainStoryboardFile 47 | Main 48 | UIRequiredDeviceCapabilities 49 | 50 | armv7 51 | 52 | UISupportedInterfaceOrientations 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationLandscapeLeft 56 | UIInterfaceOrientationLandscapeRight 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Example/InterposeExample/InterposeExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/InterposeExample/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // InterposeExample 4 | // 5 | // Copyright © 2020 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | guard let _ = (scene as? UIWindowScene) else { return } 17 | } 18 | 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Example/InterposeExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // InterposeExample 4 | // 5 | // Copyright © 2020 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Example/InterposeExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/InterposeExampleTests/InterposeExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InterposeExampleTests.swift 3 | // InterposeExampleTests 4 | // 5 | // Created by Peter Steinberger on 30.05.20. 6 | // 7 | 8 | import XCTest 9 | @testable import InterposeExample 10 | 11 | class InterposeExampleTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | gem "jazzy" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (4.2.11.3) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | algoliasearch (1.27.2) 11 | httpclient (~> 2.8, >= 2.8.3) 12 | json (>= 1.5.1) 13 | atomos (0.1.3) 14 | claide (1.0.3) 15 | cocoapods (1.9.3) 16 | activesupport (>= 4.0.2, < 5) 17 | claide (>= 1.0.2, < 2.0) 18 | cocoapods-core (= 1.9.3) 19 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 20 | cocoapods-downloader (>= 1.2.2, < 2.0) 21 | cocoapods-plugins (>= 1.0.0, < 2.0) 22 | cocoapods-search (>= 1.0.0, < 2.0) 23 | cocoapods-stats (>= 1.0.0, < 2.0) 24 | cocoapods-trunk (>= 1.4.0, < 2.0) 25 | cocoapods-try (>= 1.1.0, < 2.0) 26 | colored2 (~> 3.1) 27 | escape (~> 0.0.4) 28 | fourflusher (>= 2.3.0, < 3.0) 29 | gh_inspector (~> 1.0) 30 | molinillo (~> 0.6.6) 31 | nap (~> 1.0) 32 | ruby-macho (~> 1.4) 33 | xcodeproj (>= 1.14.0, < 2.0) 34 | cocoapods-core (1.9.3) 35 | activesupport (>= 4.0.2, < 6) 36 | algoliasearch (~> 1.0) 37 | concurrent-ruby (~> 1.1) 38 | fuzzy_match (~> 2.0.4) 39 | nap (~> 1.0) 40 | netrc (~> 0.11) 41 | typhoeus (~> 1.0) 42 | cocoapods-deintegrate (1.0.4) 43 | cocoapods-downloader (1.3.0) 44 | cocoapods-plugins (1.0.0) 45 | nap 46 | cocoapods-search (1.0.0) 47 | cocoapods-stats (1.1.0) 48 | cocoapods-trunk (1.5.0) 49 | nap (>= 0.8, < 2.0) 50 | netrc (~> 0.11) 51 | cocoapods-try (1.2.0) 52 | colored2 (3.1.2) 53 | concurrent-ruby (1.1.6) 54 | escape (0.0.4) 55 | ethon (0.12.0) 56 | ffi (>= 1.3.0) 57 | ffi (1.12.2) 58 | fourflusher (2.3.1) 59 | fuzzy_match (2.0.4) 60 | gh_inspector (1.1.3) 61 | httpclient (2.8.3) 62 | i18n (0.9.5) 63 | concurrent-ruby (~> 1.0) 64 | jazzy (0.13.3) 65 | cocoapods (~> 1.5) 66 | mustache (~> 1.1) 67 | open4 68 | redcarpet (~> 3.4) 69 | rouge (>= 2.0.6, < 4.0) 70 | sassc (~> 2.1) 71 | sqlite3 (~> 1.3) 72 | xcinvoke (~> 0.3.0) 73 | json (2.3.0) 74 | liferaft (0.0.6) 75 | minitest (5.14.1) 76 | molinillo (0.6.6) 77 | mustache (1.1.1) 78 | nanaimo (0.2.6) 79 | nap (1.1.0) 80 | netrc (0.11.0) 81 | open4 (1.3.4) 82 | redcarpet (3.5.1) 83 | rouge (3.19.0) 84 | ruby-macho (1.4.0) 85 | sassc (2.3.0) 86 | ffi (~> 1.9) 87 | sqlite3 (1.4.2) 88 | thread_safe (0.3.6) 89 | typhoeus (1.4.0) 90 | ethon (>= 0.9.0) 91 | tzinfo (1.2.7) 92 | thread_safe (~> 0.1) 93 | xcinvoke (0.3.0) 94 | liferaft (~> 0.0.6) 95 | xcodeproj (1.16.0) 96 | CFPropertyList (>= 2.3.3, < 4.0) 97 | atomos (~> 0.1.3) 98 | claide (>= 1.0.2, < 2.0) 99 | colored2 (~> 3.1) 100 | nanaimo (~> 0.2.6) 101 | 102 | PLATFORMS 103 | ruby 104 | 105 | DEPENDENCIES 106 | cocoapods 107 | jazzy 108 | 109 | BUNDLED WITH 110 | 2.1.4 111 | -------------------------------------------------------------------------------- /InterposeKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'InterposeKit' 3 | s.version = '0.0.2' 4 | s.summary = 'A modern library to swizzle elegantly in Swift.' 5 | s.homepage = 'https://github.com/steipete/InterposeKit' 6 | s.source = { :git => s.homepage + '.git', :tag => s.version } 7 | s.license = { :type => 'MIT', :file => 'LICENSE' } 8 | s.authors = { 'Peter Steinberger' => 'steipete@gmail.com' } 9 | s.source_files = 'Sources/**/*.{h,c,swift}' 10 | s.swift_versions = ['5.2'] 11 | s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' } 12 | s.ios.deployment_target = '11.0' 13 | s.osx.deployment_target = '10.13' 14 | s.tvos.deployment_target = '11.0' 15 | s.watchos.deployment_target = '5.0' 16 | end 17 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/Info-Tests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 36 | 37 | 38 | 39 | 41 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /InterposeKit.xcodeproj/xcshareddata/xcschemes/InterposeTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 44 | 45 | 51 | 52 | 58 | 59 | 60 | 61 | 63 | 64 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /InterposeKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /InterposeKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /InterposeKit.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "InterposeKit", 6 | "repositoryURL": "https://github.com/steipete/InterposeKit", 7 | "state": { 8 | "branch": "master", 9 | "revision": "1d6023343c70f1101e5e941440db8fdf9e862058", 10 | "version": null 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /InterposeTestHost/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | @UIApplicationMain 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | window = UIWindow(frame: UIScreen.main.bounds) 10 | window!.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()! 11 | (window!.rootViewController as? UINavigationController)?.topViewController?.title = "Test Host" 12 | window!.makeKeyAndVisible() 13 | return true 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /InterposeTestHost/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /InterposeTestHost/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /InterposeTestHost/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /InterposeTestHost/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /InterposeTestHost/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSApplicationCategoryType 22 | public.app-category.developer-tools 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /InterposeTestHost/InterposeTestHost.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /InterposeTestHost/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // InterposeTestHost 4 | // 5 | // Created by Peter Steinberger on 07.06.20. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Peter Steinberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "InterposeKit", 6 | platforms: [ 7 | .iOS(.v11), 8 | .macOS(.v10_13), 9 | .tvOS(.v11), 10 | .watchOS(.v5) 11 | ], 12 | products: [ 13 | .library(name: "InterposeKit", targets: ["InterposeKit"]), 14 | ], 15 | targets: [ 16 | .target(name: "SuperBuilder"), 17 | .target(name: "InterposeKit", dependencies: ["SuperBuilder"]), 18 | .testTarget(name: "InterposeKitTests", dependencies: ["InterposeKit"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | InterposeKit 2 | 3 | [![SwiftPM](https://github.com/steipete/InterposeKit/workflows/SwiftPM/badge.svg)](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM) 4 | [![xcodebuild](https://github.com/steipete/InterposeKit/workflows/xcodebuild/badge.svg)](https://github.com/steipete/InterposeKit/actions?query=workflow%3Axcodebuild) 5 | [![pod lib lint](https://github.com/steipete/InterposeKit/workflows/pod%20lib%20lint/badge.svg)](https://github.com/steipete/InterposeKit/actions?query=workflow%3A%22pod+lib+lint%22) 6 | ![Xcode 11.4+](https://img.shields.io/badge/Xcode-11.4%2B-blue.svg) 7 | ![Swift 5.2+](https://img.shields.io/badge/Swift-5.2%2B-orange.svg) 8 | 10 | 11 | InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is [well-documented](http://interposekit.com/), [tested](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM), written in "pure" Swift 5.2 and works on `@objc dynamic` Swift functions or Objective-C instance methods. The Inspiration for InterposeKit was [a race condition in Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/), which required tricky swizzling to fix, I also wrote up [implementation thoughts on my blog](https://steipete.com/posts/interposekit/). 12 | 13 | Instead of [adding new methods and exchanging implementations](https://nshipster.com/method-swizzling/) based on [`method_exchangeImplementations`](https://developer.apple.com/documentation/objectivec/1418769-method_exchangeimplementations), this library replaces the implementation directly using [`class_replaceMethod`](https://developer.apple.com/documentation/objectivec/1418677-class_replacemethod). This avoids some of [the usual problems with swizzling](https://pspdfkit.com/blog/2019/swizzling-in-swift/). 14 | 15 | You can call the original implementation and add code before, instead or after a method call. 16 | This is similar to the [Aspects library](https://github.com/steipete/Aspects), but doesn't yet do dynamic subclassing. 17 | 18 | Compare: [Swizzling a property without helper and with InterposeKit](https://gist.github.com/steipete/f955aaa0742021af15add0133d8482b9) 19 | 20 | ## Usage 21 | 22 | Let's say you want to amend `sayHi` from `TestClass`: 23 | 24 | ```swift 25 | class TestClass: NSObject { 26 | // Functions need to be marked as `@objc dynamic` or written in Objective-C. 27 | @objc dynamic func sayHi() -> String { 28 | print("Calling sayHi") 29 | return "Hi there 👋" 30 | } 31 | } 32 | 33 | let interposer = try Interpose(TestClass.self) { 34 | try $0.prepareHook( 35 | #selector(TestClass.sayHi), 36 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 37 | hookSignature: (@convention(block) (AnyObject) -> String).self) { 38 | store in { `self` in 39 | print("Before Interposing \(`self`)") 40 | let string = store.original(`self`, store.selector) // free to skip 41 | print("After Interposing \(`self`)") 42 | return string + "and Interpose" 43 | } 44 | } 45 | } 46 | 47 | // Don't need the hook anymore? Undo is built-in! 48 | interposer.revert() 49 | ``` 50 | 51 | Want to hook just a single instance? No problem! 52 | 53 | ```swift 54 | let hook = try testObj.hook( 55 | #selector(TestClass.sayHi), 56 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 57 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in 58 | return store.original(`self`, store.selector) + "just this instance" 59 | } 60 | } 61 | ``` 62 | 63 | Here's what we get when calling `print(TestClass().sayHi())` 64 | ``` 65 | [Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020 66 | Before Interposing 67 | Calling sayHi 68 | After Interposing 69 | Hi there 👋 and Interpose 70 | ``` 71 | 72 | ## Key Features 73 | 74 | - Interpose directly modifies the implementation of a `Method`, which is [safer than selector-based swizzling]((https://pspdfkit.com/blog/2019/swizzling-in-swift/)). 75 | - Interpose works on classes and individual objects. 76 | - Hooks can easily be undone via calling `revert()`. This also checks and errors if someone else changed stuff in between. 77 | - Mostly Swift, no `NSInvocation`, which requires boxing and can be slow. 78 | - No Type checking. If you have a typo or forget a `convention` part, this will crash at runtime. 79 | - Yes, you have to type the resulting type twice This is a tradeoff, else we need `NSInvocation`. 80 | - Delayed Interposing helps when a class is loaded at runtime. This is useful for [Mac Catalyst](https://steipete.com/posts/mac-catalyst-crash-hunt/). 81 | 82 | ## Object Hooking 83 | 84 | InterposeKit can hook classes and object. Class hooking is similar to swizzling, but object-based hooking offers a variety of new ways to set hooks. This is achieved via creating a dynamic subclass at runtime. 85 | 86 | Caveat: Hooking will fail with an error if the object uses KVO. The KVO machinery is fragile and it's to easy to cause a crash. Using KVO after a hook was created is supported and will not cause issues. 87 | 88 | ## Various ways to define the signature 89 | 90 | Next to using `methodSignature` and `hookSignature`, following variants to define the signature are also possible: 91 | 92 | ### methodSignature + casted block 93 | ``` 94 | let interposer = try Interpose(testObj) { 95 | try $0.hook( 96 | #selector(TestClass.sayHi), 97 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in 98 | let string = store.original(`self`, store.selector) 99 | return string + testString 100 | } as @convention(block) (AnyObject) -> String } 101 | } 102 | ``` 103 | 104 | ### Define type via store object 105 | ``` 106 | // Functions need to be `@objc dynamic` to be hookable. 107 | let interposer = try Interpose(testObj) { 108 | try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in { 109 | 110 | // You're free to skip calling the original implementation. 111 | let int = store.original($0, store.selector) 112 | return int + returnIntOverrideOffset 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | ## Delayed Hooking 119 | 120 | Sometimes it can be necessary to hook a class deep in a system framework, which is loaded at a later time. Interpose has a solution for this and uses a hook in the dynamic linker to be notified whenever new classes are loaded. 121 | 122 | ```swift 123 | try Interpose.whenAvailable(["RTIInput", "SystemSession"]) { 124 | let lock = DispatchQueue(label: "com.steipete.document-state-hack") 125 | try $0.hook("documentState", { store in { `self` in 126 | lock.sync { 127 | store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector) 128 | }} as @convention(block) (AnyObject) -> AnyObject}) 129 | 130 | try $0.hook("setDocumentState:", { store in { `self`, newValue in 131 | lock.sync { 132 | store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue) 133 | }} as @convention(block) (AnyObject, AnyObject) -> Void}) 134 | } 135 | ``` 136 | 137 | 138 | ## FAQ 139 | 140 | ### Why didn't you call it Interpose? "Kit" feels so old-school. 141 | Naming it Interpose was the plan, but then [SR-898](https://bugs.swift.org/browse/SR-898) came. While having a class with the same name as the module works [in most cases](https://forums.swift.org/t/frameworkname-is-not-a-member-type-of-frameworkname-errors-inside-swiftinterface/28962), [this breaks](https://twitter.com/BalestraPatrick/status/1260928023357878273) when you enable build-for-distribution. There's some [discussion](https://forums.swift.org/t/pitch-fully-qualified-name-syntax/28482/81) to get that fixed, but this will be more towards end of 2020, if even. 142 | 143 | ### I want to hook into Swift! You made another ObjC swizzle thingy, why? 144 | UIKit and AppKit won't go away, and the bugs won't go away either. I see this as a rarely-needed instrument to fix system-level issues. There are ways to do some of that in Swift, but that's a separate (and much more difficult!) project. (See [Dynamic function replacement #20333](https://github.com/apple/swift/pull/20333) aka `@_dynamicReplacement` for details.) 145 | 146 | ### Can I ship this? 147 | Yes, absolutely. The goal for this one project is a simple library that doesn't try to be too smart. I did this in [Aspects](https://github.com/steipete/Aspects) and while I loved this to no end, it's problematic and can cause side-effects with other code that tries to be clever. InterposeKit is boring, so you don't have to worry about conditions like "We added New Relic to our app and now [your thing crashes](https://github.com/steipete/Aspects/issues/21)". 148 | 149 | ### It does not do X! 150 | Pull Requests welcome! You might wanna open a draft before to lay out what you plan, I want to keep the feature-set minimal so it stays simple and no-magic. 151 | 152 | ## Installation 153 | 154 | Building InterposeKit requires Xcode 11.4+ or a Swift 5.2+ toolchain with the Swift Package Manager. 155 | 156 | ### Swift Package Manager 157 | 158 | Add `.package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1")` to your 159 | `Package.swift` file's `dependencies`. 160 | 161 | ### CocoaPods 162 | 163 | [InterposeKit is on CocoaPods](https://cocoapods.org/pods/InterposeKit). Add `pod 'InterposeKit'` to your `Podfile`. 164 | 165 | ### Carthage 166 | 167 | Add `github "steipete/InterposeKit"` to your `Cartfile`. 168 | 169 | ## Improvement Ideas 170 | 171 | - Write proposal to allow to [convert the calling convention of existing types](https://twitter.com/steipete/status/1266799174563041282?s=21). 172 | - Use the C block struct to perform type checking between Method type and C type (I do that in [Aspects library](https://github.com/steipete/Aspects)), it's still a runtime crash but could be at hook time, not when we call it. 173 | - Add a way to get all current hooks from an object/class. 174 | - Add a way to revert hooks without super helper. 175 | - Add a way to apply multiple hooks to classes 176 | - Enable hooking of class methods. 177 | - Add [dyld_dynamic_interpose](https://twitter.com/steipete/status/1258482647933870080) to hook pure C functions 178 | - Combine Promise-API for `Interpose.whenAvailable` for better error bubbling. 179 | - Experiment with [Swift function hooking](https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift)? ⚡️ 180 | - Test against Swift Nightly as Cron Job 181 | - Switch to Trampolines to manage cases where other code overrides super, so we end up with a super call that's [not on top of the class hierarchy](https://github.com/steipete/InterposeKit/pull/15#discussion_r439871752). 182 | - I'm sure there's more - Pull Requests or [comments](https://twitter.com/steipete) very welcome! 183 | 184 | Make this happen: 185 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 186 | ![CocoaPods](https://img.shields.io/cocoapods/v/SwiftyJSON.svg) 187 | 188 | ## Thanks 189 | 190 | Special thanks to [JP Simard](https://github.com/jpsim/Yams) who did such a great job in setting up [Yams](https://github.com/jpsim/Yams) with GitHub Actions - this was extremely helpful to build CI here fast. 191 | 192 | ## License 193 | 194 | InterposeKit is MIT Licensed. 195 | -------------------------------------------------------------------------------- /Sources/InterposeKit/AnyHook.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// Base class, represents a hook to exactly one method. 4 | public class AnyHook { 5 | /// The class this hook is based on. 6 | public let `class`: AnyClass 7 | 8 | /// The selector this hook interposes. 9 | public let selector: Selector 10 | 11 | /// The current state of the hook. 12 | public internal(set) var state = State.prepared 13 | 14 | // else we validate init order 15 | var replacementIMP: IMP! 16 | 17 | // fetched at apply time, changes late, thus class requirement 18 | var origIMP: IMP? 19 | 20 | /// The possible task states 21 | public enum State: Equatable { 22 | /// The task is prepared to be nterposed. 23 | case prepared 24 | 25 | /// The method has been successfully interposed. 26 | case interposed 27 | 28 | /// An error happened while interposing a method. 29 | indirect case error(InterposeError) 30 | } 31 | 32 | init(`class`: AnyClass, selector: Selector) throws { 33 | self.selector = selector 34 | self.class = `class` 35 | 36 | // Check if method exists 37 | try validate() 38 | } 39 | 40 | func replaceImplementation() throws { 41 | preconditionFailure("Not implemented") 42 | } 43 | 44 | func resetImplementation() throws { 45 | preconditionFailure("Not implemented") 46 | } 47 | 48 | /// Apply the interpose hook. 49 | @discardableResult public func apply() throws -> AnyHook { 50 | try execute(newState: .interposed) { try replaceImplementation() } 51 | return self 52 | } 53 | 54 | /// Revert the interpose hoook. 55 | @discardableResult public func revert() throws -> AnyHook { 56 | try execute(newState: .prepared) { try resetImplementation() } 57 | return self 58 | } 59 | 60 | /// Validate that the selector exists on the active class. 61 | @discardableResult func validate(expectedState: State = .prepared) throws -> Method { 62 | guard let method = class_getInstanceMethod(`class`, selector) else { 63 | throw InterposeError.methodNotFound(`class`, selector) 64 | } 65 | guard state == expectedState else { throw InterposeError.invalidState(expectedState: expectedState) } 66 | return method 67 | } 68 | 69 | private func execute(newState: State, task: () throws -> Void) throws { 70 | do { 71 | try task() 72 | state = newState 73 | } catch let error as InterposeError { 74 | state = .error(error) 75 | throw error 76 | } 77 | } 78 | 79 | /// Release the hook block if possible. 80 | public func cleanup() { 81 | switch state { 82 | case .prepared: 83 | Interpose.log("Releasing -[\(`class`).\(selector)] IMP: \(replacementIMP!)") 84 | imp_removeBlock(replacementIMP) 85 | case .interposed: 86 | Interpose.log("Keeping -[\(`class`).\(selector)] IMP: \(replacementIMP!)") 87 | case let .error(error): 88 | Interpose.log("Leaking -[\(`class`).\(selector)] IMP: \(replacementIMP!) due to error: \(error)") 89 | } 90 | } 91 | } 92 | 93 | /// Hook baseclass with generic signatures. 94 | public class TypedHook: AnyHook { 95 | /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. 96 | public var original: MethodSignature { 97 | preconditionFailure("Always override") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/InterposeKit/ClassHook.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Interpose { 4 | /// A hook to an instance method and stores both the original and new implementation. 5 | final public class ClassHook: TypedHook { 6 | /* HookSignature?: This must be optional or swift runtime will crash. 7 | Or swiftc may segfault. Compiler bug? */ 8 | /// Initialize a new hook to interpose an instance method. 9 | public init(`class`: AnyClass, selector: Selector, 10 | implementation: (ClassHook) -> HookSignature?) throws { 11 | try super.init(class: `class`, selector: selector) 12 | replacementIMP = imp_implementationWithBlock(implementation(self) as Any) 13 | } 14 | 15 | override func replaceImplementation() throws { 16 | let method = try validate() 17 | origIMP = class_replaceMethod(`class`, selector, replacementIMP, method_getTypeEncoding(method)) 18 | guard origIMP != nil else { throw InterposeError.nonExistingImplementation(`class`, selector) } 19 | Interpose.log("Swizzled -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") 20 | } 21 | 22 | override func resetImplementation() throws { 23 | let method = try validate(expectedState: .interposed) 24 | precondition(origIMP != nil) 25 | let previousIMP = class_replaceMethod(`class`, selector, origIMP!, method_getTypeEncoding(method)) 26 | guard previousIMP == replacementIMP else { 27 | throw InterposeError.unexpectedImplementation(`class`, selector, previousIMP) 28 | } 29 | Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") 30 | } 31 | 32 | /// The original implementation is cached at hook time. 33 | public override var original: MethodSignature { 34 | unsafeBitCast(origIMP, to: MethodSignature.self) 35 | } 36 | } 37 | } 38 | 39 | #if DEBUG 40 | extension Interpose.ClassHook: CustomDebugStringConvertible { 41 | public var debugDescription: String { 42 | return "\(selector) -> \(String(describing: origIMP))" 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/InterposeKit/HookFinder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Interpose { 4 | 5 | private struct AssociatedKeys { 6 | static var hookForBlock: UInt8 = 0 7 | } 8 | 9 | private class WeakObjectContainer: NSObject { 10 | private weak var _object: T? 11 | 12 | var object: T? { 13 | return _object 14 | } 15 | init(with object: T?) { 16 | _object = object 17 | } 18 | } 19 | 20 | static func storeHook(hook: HookType, to block: AnyObject) { 21 | // Weakly store reference to hook inside the block of the IMP. 22 | objc_setAssociatedObject(block, &AssociatedKeys.hookForBlock, 23 | WeakObjectContainer(with: hook), .OBJC_ASSOCIATION_RETAIN) 24 | 25 | } 26 | 27 | // Finds the hook to a given implementation. 28 | static func hookForIMP(_ imp: IMP) -> HookType? { 29 | // Get the block that backs our IMP replacement 30 | guard let block = imp_getBlock(imp) else { return nil } 31 | let container = objc_getAssociatedObject(block, &AssociatedKeys.hookForBlock) as? WeakObjectContainer 32 | return container?.object 33 | } 34 | 35 | // Find the hook above us (not necessarily topmost) 36 | static func findNextHook(selfHook: HookType, topmostIMP: IMP) -> HookType? { 37 | // We are not topmost hook, so find the hook above us! 38 | var impl: IMP? = topmostIMP 39 | var currentHook: HookType? 40 | repeat { 41 | // get topmost hook 42 | let hook: HookType? = Interpose.hookForIMP(impl!) 43 | if hook === selfHook { 44 | // return parent 45 | return currentHook 46 | } 47 | // crawl down the chain until we find ourselves 48 | currentHook = hook 49 | impl = hook?.origIMP 50 | } while impl != nil 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/InterposeKit/InterposeError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// The list of errors while hooking a method. 4 | public enum InterposeError: LocalizedError { 5 | /// The method couldn't be found. Usually happens for when you use stringified selectors that do not exist. 6 | case methodNotFound(AnyClass, Selector) 7 | 8 | /// The implementation could not be found. Class must be in a weird state for this to happen. 9 | case nonExistingImplementation(AnyClass, Selector) 10 | 11 | /// Someone else changed the implementation; reverting removed this implementation. 12 | /// This is bad, likely someone else also hooked this method. If you are in such a codebase, do not use revert. 13 | case unexpectedImplementation(AnyClass, Selector, IMP?) 14 | 15 | /// Unable to register subclass for object-based interposing. 16 | case failedToAllocateClassPair(class: AnyClass, subclassName: String) 17 | 18 | /// Unable to add method for object-based interposing. 19 | case unableToAddMethod(AnyClass, Selector) 20 | 21 | /// Object-based hooking does not work if an object is using KVO. 22 | /// The KVO mechanism also uses subclasses created at runtime but doesn't check for additional overrides. 23 | /// Adding a hook eventually crashes the KVO management code so we reject hooking altogether in this case. 24 | case keyValueObservationDetected(AnyObject) 25 | 26 | /// Object is lying about it's actual class metadata. 27 | /// This usually happens when other swizzling libraries (like Aspects) also interfere with a class. 28 | /// While this might just work, it's not worth risking a crash, so similar to KVO this case is rejected. 29 | /// 30 | /// @note Printing classes in Swift uses the class posing mechanism. 31 | /// Use `NSClassFromString` to get the correct name. 32 | case objectPosingAsDifferentClass(AnyObject, actualClass: AnyClass) 33 | 34 | /// Can't revert or apply if already done so. 35 | case invalidState(expectedState: AnyHook.State) 36 | 37 | /// Unable to remove hook. 38 | case resetUnsupported(_ reason: String) 39 | 40 | /// Generic failure 41 | case unknownError(_ reason: String) 42 | } 43 | 44 | extension InterposeError: Equatable { 45 | // Lazy equating via string compare 46 | public static func == (lhs: InterposeError, rhs: InterposeError) -> Bool { 47 | return lhs.errorDescription == rhs.errorDescription 48 | } 49 | 50 | public var errorDescription: String? { 51 | switch self { 52 | case .methodNotFound(let klass, let selector): 53 | return "Method not found: -[\(klass) \(selector)]" 54 | case .nonExistingImplementation(let klass, let selector): 55 | return "Implementation not found: -[\(klass) \(selector)]" 56 | case .unexpectedImplementation(let klass, let selector, let IMP): 57 | return "Unexpected Implementation in -[\(klass) \(selector)]: \(String(describing: IMP))" 58 | case .failedToAllocateClassPair(let klass, let subclassName): 59 | return "Failed to allocate class pair: \(klass), \(subclassName)" 60 | case .unableToAddMethod(let klass, let selector): 61 | return "Unable to add method: -[\(klass) \(selector)]" 62 | case .keyValueObservationDetected(let obj): 63 | return "Unable to hook object that uses Key Value Observing: \(obj)" 64 | case .objectPosingAsDifferentClass(let obj, let actualClass): 65 | return "Unable to hook \(type(of: obj)) posing as \(NSStringFromClass(actualClass))/" 66 | case .invalidState(let expectedState): 67 | return "Invalid State. Expected: \(expectedState)" 68 | case .resetUnsupported(let reason): 69 | return "Reset Unsupported: \(reason)" 70 | case .unknownError(let reason): 71 | return reason 72 | } 73 | } 74 | 75 | @discardableResult func log() -> InterposeError { 76 | Interpose.log(self.errorDescription!) 77 | return self 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/InterposeKit/InterposeKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // InterposeKit.h 3 | // InterposeKit 4 | // 5 | // Copyright © 2020 Peter Steinberger. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for InterposeKit. 11 | FOUNDATION_EXPORT double InterposeKitVersionNumber; 12 | 13 | //! Project version string for InterposeKit. 14 | FOUNDATION_EXPORT const unsigned char InterposeKitVersionString[]; 15 | -------------------------------------------------------------------------------- /Sources/InterposeKit/InterposeKit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSObject { 4 | /// Hook an `@objc dynamic` instance method via selector on the current object or class.. 5 | @discardableResult public func hook ( 6 | _ selector: Selector, 7 | methodSignature: MethodSignature.Type = MethodSignature.self, 8 | hookSignature: HookSignature.Type = HookSignature.self, 9 | _ implementation: (TypedHook) -> HookSignature?) throws -> AnyHook { 10 | 11 | if let klass = self as? AnyClass { 12 | return try Interpose.ClassHook(class: klass, selector: selector, implementation: implementation).apply() 13 | } else { 14 | return try Interpose.ObjectHook(object: self, selector: selector, implementation: implementation).apply() 15 | } 16 | } 17 | 18 | /// Hook an `@objc dynamic` instance method via selector on the current object or class.. 19 | @discardableResult public class func hook ( 20 | _ selector: Selector, 21 | methodSignature: MethodSignature.Type = MethodSignature.self, 22 | hookSignature: HookSignature.Type = HookSignature.self, 23 | _ implementation: (TypedHook) -> HookSignature?) throws -> AnyHook { 24 | return try Interpose.ClassHook(class: self as AnyClass, 25 | selector: selector, implementation: implementation).apply() 26 | } 27 | } 28 | 29 | /// Interpose is a modern library to swizzle elegantly in Swift. 30 | /// 31 | /// Methods are hooked via replacing the implementation, instead of the usual exchange. 32 | /// Supports both swizzling classes and individual objects. 33 | final public class Interpose { 34 | /// Stores swizzle hooks and executes them at once. 35 | public let `class`: AnyClass 36 | /// Lists all hooks for the current interpose class object. 37 | public private(set) var hooks: [AnyHook] = [] 38 | 39 | /// If Interposing is object-based, this is set. 40 | public let object: AnyObject? 41 | 42 | // Checks if a object is posing as a different class 43 | // via implementing 'class' and returning something else. 44 | private func checkObjectPosingAsDifferentClass(_ object: AnyObject) -> AnyClass? { 45 | let perceivedClass: AnyClass = type(of: object) 46 | let actualClass: AnyClass = object_getClass(object)! 47 | if actualClass != perceivedClass { 48 | return actualClass 49 | } 50 | return nil 51 | } 52 | 53 | // This is based on observation, there is no documented way 54 | private func isKVORuntimeGeneratedClass(_ klass: AnyClass) -> Bool { 55 | NSStringFromClass(klass).hasPrefix("NSKVO") 56 | } 57 | 58 | /// Initializes an instance of Interpose for a specific class. 59 | /// If `builder` is present, `apply()` is automatically called. 60 | public init(_ `class`: AnyClass, builder: ((Interpose) throws -> Void)? = nil) throws { 61 | self.class = `class` 62 | self.object = nil 63 | 64 | // Only apply if a builder is present 65 | if let builder = builder { 66 | try apply(builder) 67 | } 68 | } 69 | 70 | /// Initialize with a single object to interpose. 71 | public init(_ object: NSObject, builder: ((Interpose) throws -> Void)? = nil) throws { 72 | self.object = object 73 | self.class = type(of: object) 74 | 75 | if let actualClass = checkObjectPosingAsDifferentClass(object) { 76 | if isKVORuntimeGeneratedClass(actualClass) { 77 | throw InterposeError.keyValueObservationDetected(object) 78 | } else { 79 | throw InterposeError.objectPosingAsDifferentClass(object, actualClass: actualClass) 80 | } 81 | } 82 | 83 | // Only apply if a builder is present 84 | if let builder = builder { 85 | try apply(builder) 86 | } 87 | } 88 | 89 | deinit { 90 | hooks.forEach({ $0.cleanup() }) 91 | } 92 | 93 | /// Hook an `@objc dynamic` instance method via selector name on the current class. 94 | @discardableResult public func hook( 95 | _ selName: String, 96 | methodSignature: MethodSignature.Type = MethodSignature.self, 97 | hookSignature: HookSignature.Type = HookSignature.self, 98 | _ implementation: (TypedHook) -> HookSignature?) 99 | throws -> TypedHook { 100 | try hook(NSSelectorFromString(selName), 101 | methodSignature: methodSignature, hookSignature: hookSignature, implementation) 102 | } 103 | 104 | /// Hook an `@objc dynamic` instance method via selector on the current class. 105 | @discardableResult public func hook ( 106 | _ selector: Selector, 107 | methodSignature: MethodSignature.Type = MethodSignature.self, 108 | hookSignature: HookSignature.Type = HookSignature.self, 109 | _ implementation: (TypedHook) -> HookSignature?) 110 | throws -> TypedHook { 111 | let hook = try prepareHook(selector, methodSignature: methodSignature, 112 | hookSignature: hookSignature, implementation) 113 | try hook.apply() 114 | return hook 115 | 116 | } 117 | 118 | /// Prepares a hook, but does not call apply immediately. 119 | @discardableResult public func prepareHook ( 120 | _ selector: Selector, 121 | methodSignature: MethodSignature.Type = MethodSignature.self, 122 | hookSignature: HookSignature.Type = HookSignature.self, 123 | _ implementation: (TypedHook) -> HookSignature?) 124 | throws -> TypedHook { 125 | var hook: TypedHook 126 | if let object = self.object { 127 | hook = try ObjectHook(object: object, selector: selector, implementation: implementation) 128 | } else { 129 | hook = try ClassHook(class: `class`, selector: selector, implementation: implementation) 130 | } 131 | hooks.append(hook) 132 | return hook 133 | } 134 | 135 | /// Apply all stored hooks. 136 | @discardableResult public func apply(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { 137 | try execute(hook) { try $0.apply() } 138 | } 139 | 140 | /// Revert all stored hooks. 141 | @discardableResult public func revert(_ hook: ((Interpose) throws -> Void)? = nil) throws -> Interpose { 142 | try execute(hook, expectedState: .interposed) { try $0.revert() } 143 | } 144 | 145 | private func execute(_ task: ((Interpose) throws -> Void)? = nil, 146 | expectedState: AnyHook.State = .prepared, 147 | executor: ((AnyHook) throws -> Void)) throws -> Interpose { 148 | // Run pre-apply code first 149 | if let task = task { 150 | try task(self) 151 | } 152 | // Validate all tasks, stop if anything is not valid 153 | guard hooks.allSatisfy({ 154 | (try? $0.validate(expectedState: expectedState)) != nil 155 | }) else { 156 | throw InterposeError.invalidState(expectedState: expectedState) 157 | } 158 | // Execute all tasks 159 | try hooks.forEach(executor) 160 | return self 161 | } 162 | } 163 | 164 | // MARK: Logging 165 | 166 | extension Interpose { 167 | /// Logging uses print and is minimal. 168 | public static var isLoggingEnabled = false 169 | 170 | /// Simple log wrapper for print. 171 | class func log(_ object: Any) { 172 | if isLoggingEnabled { 173 | print("[Interposer] \(object)") 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/InterposeKit/InterposeSubclass.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class InterposeSubclass { 4 | 5 | private enum Constants { 6 | static let subclassSuffix = "InterposeKit_" 7 | } 8 | 9 | enum ObjCSelector { 10 | static let getClass = Selector((("class"))) 11 | } 12 | 13 | enum ObjCMethodEncoding { 14 | static let getClass = extract("#@:") 15 | 16 | private static func extract(_ string: StaticString) -> UnsafePointer { 17 | return UnsafeRawPointer(string.utf8Start).assumingMemoryBound(to: CChar.self) 18 | } 19 | } 20 | 21 | /// The object that is being hooked. 22 | let object: AnyObject 23 | 24 | /// Subclass that we create on the fly 25 | private(set) var dynamicClass: AnyClass 26 | 27 | /// If the class has been altered (e.g. via NSKVONotifying_ KVO logic) 28 | /// then perceived and actual class don't match. 29 | /// 30 | /// Making KVO and Object-based hooking work at the same time is difficult. 31 | /// If we make a dynamic subclass over KVO, invalidating the token crashes in cache_getImp. 32 | init(object: AnyObject) throws { 33 | self.object = object 34 | dynamicClass = type(of: object) // satisfy set to something 35 | dynamicClass = try getExistingSubclass() ?? createSubclass() 36 | } 37 | 38 | private func createSubclass() throws -> AnyClass { 39 | let perceivedClass: AnyClass = type(of: object) 40 | let actualClass: AnyClass = object_getClass(object)! 41 | 42 | let className = NSStringFromClass(perceivedClass) 43 | // Right now we are wasteful. Might be able to optimize for shared IMP? 44 | let uuid = UUID().uuidString.replacingOccurrences(of: "-", with: "") 45 | let subclassName = Constants.subclassSuffix + className + uuid 46 | 47 | let subclass: AnyClass? = subclassName.withCString { cString in 48 | // swiftlint:disable:next force_cast 49 | if let existingClass = objc_getClass(cString) as! AnyClass? { 50 | return existingClass 51 | } else { 52 | guard let subclass: AnyClass = objc_allocateClassPair(actualClass, cString, 0) else { return nil } 53 | replaceGetClass(in: subclass, decoy: perceivedClass) 54 | objc_registerClassPair(subclass) 55 | return subclass 56 | } 57 | } 58 | 59 | guard let nnSubclass = subclass else { 60 | throw InterposeError.failedToAllocateClassPair(class: perceivedClass, subclassName: subclassName) 61 | } 62 | 63 | object_setClass(object, nnSubclass) 64 | let oldName = NSStringFromClass(class_getSuperclass(object_getClass(object)!)!) 65 | Interpose.log("Generated \(NSStringFromClass(nnSubclass)) for object (was: \(oldName))") 66 | return nnSubclass 67 | } 68 | 69 | /// We need to reuse a dynamic subclass if the object already has one. 70 | private func getExistingSubclass() -> AnyClass? { 71 | let actualClass: AnyClass = object_getClass(object)! 72 | if NSStringFromClass(actualClass).hasPrefix(Constants.subclassSuffix) { 73 | return actualClass 74 | } 75 | return nil 76 | } 77 | 78 | #if !os(Linux) 79 | private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) { 80 | // crashes on linux 81 | let getClass: @convention(block) (AnyObject) -> AnyClass = { _ in 82 | perceivedClass 83 | } 84 | let impl = imp_implementationWithBlock(getClass as Any) 85 | _ = class_replaceMethod(`class`, ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) 86 | _ = class_replaceMethod(object_getClass(`class`), ObjCSelector.getClass, impl, ObjCMethodEncoding.getClass) 87 | } 88 | 89 | class var supportsSuperTrampolines: Bool { 90 | NSClassFromString("SuperBuilder")?.value(forKey: "isSupportedArchitecure") as? Bool ?? false 91 | } 92 | 93 | private lazy var addSuperImpl: @convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool = { 94 | let handle = dlopen(nil, RTLD_LAZY) 95 | let imp = dlsym(handle, "IKTAddSuperImplementationToClass") 96 | return unsafeBitCast(imp, to: (@convention(c) (AnyClass, Selector, NSErrorPointer) -> Bool).self) 97 | }() 98 | 99 | func addSuperTrampoline(selector: Selector) { 100 | var error: NSError? 101 | if addSuperImpl(dynamicClass, selector, &error) == false { 102 | Interpose.log("Failed to add super implementation to -[\(dynamicClass).\(selector)]: \(error!)") 103 | } else { 104 | let imp = class_getMethodImplementation(dynamicClass, selector)! 105 | Interpose.log("Added super for -[\(dynamicClass).\(selector)]: \(imp)") 106 | } 107 | } 108 | #else 109 | func addSuperTrampoline(selector: Selector) { } 110 | class var supportsSuperTrampolines: Bool { return false } 111 | private func replaceGetClass(in class: AnyClass, decoy perceivedClass: AnyClass) {} 112 | #endif 113 | } 114 | -------------------------------------------------------------------------------- /Sources/InterposeKit/LinuxCompileSupport.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Linux is used to create Jazzy docs 4 | #if os(Linux) 5 | /// :nodoc: Selector 6 | public struct Selector: Equatable { 7 | var name: String? 8 | init(_ name: String) { self.name = name } 9 | } 10 | /// :nodoc: IMP 11 | public struct IMP: Equatable {} 12 | /// :nodoc: Method 13 | public struct Method {} 14 | func NSSelectorFromString(_ aSelectorName: String) -> Selector { Selector("") } 15 | func class_getInstanceMethod(_ cls: AnyClass?, _ name: Selector) -> Method? { return nil } 16 | func class_getMethodImplementation(_ cls: AnyClass?, _ name: Selector) -> IMP? { return nil } 17 | func class_replaceMethod(_ cls: AnyClass?, _ name: Selector, 18 | _ imp: IMP, _ types: UnsafePointer?) -> IMP? { IMP() } 19 | func class_addMethod(_ cls: AnyClass?, _ name: Selector, 20 | _ imp: IMP, _ types: UnsafePointer?) -> Bool { return false } 21 | func class_copyMethodList(_ cls: AnyClass?, 22 | _ outCount: UnsafeMutablePointer?) -> UnsafeMutablePointer? { return nil } 23 | func object_getClass(_ obj: Any?) -> AnyClass? { return nil } 24 | @discardableResult func object_setClass(_ obj: Any?, _ cls: AnyClass) -> AnyClass? { return nil } 25 | func method_getName(_ method: Method) -> Selector { Selector("") } 26 | func class_getSuperclass(_ cls: AnyClass?) -> AnyClass? { return nil } 27 | func method_getTypeEncoding(_ method: Method) -> UnsafePointer? { return nil } 28 | func method_getImplementation(_ method: Method) -> IMP { IMP() } 29 | // swiftlint:disable:next identifier_name 30 | func _dyld_register_func_for_add_image(_ func: 31 | (@convention(c) (UnsafePointer?, Int) -> Void)!) {} 32 | func objc_allocateClassPair(_ superclass: AnyClass?, 33 | _ name: UnsafePointer, 34 | _ extraBytes: Int) -> AnyClass? { return nil } 35 | func objc_registerClassPair(_ cls: AnyClass) {} 36 | func objc_getClass(_: UnsafePointer!) -> Any! { return nil } 37 | func imp_implementationWithBlock(_ block: Any) -> IMP { IMP() } 38 | func imp_getBlock(_ anImp: IMP) -> Any? { return nil } 39 | @discardableResult func imp_removeBlock(_ anImp: IMP) -> Bool { false } 40 | @objc class NSError: NSObject {} 41 | // AutoreleasingUnsafeMutablePointer is not available on Linux. 42 | typealias NSErrorPointer = UnsafeMutablePointer? 43 | extension NSObject { 44 | /// :nodoc: value 45 | open func value(forKey key: String) -> Any? { return nil } 46 | } 47 | /// :nodoc: objc_AssociationPolicy 48 | // swiftlint:disable:next type_name 49 | enum objc_AssociationPolicy: UInt { 50 | // swiftlint:disable:next identifier_name 51 | case OBJC_ASSOCIATION_ASSIGN = 0 52 | // swiftlint:disable:next identifier_name 53 | case OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1 54 | // swiftlint:disable:next identifier_name 55 | case OBJC_ASSOCIATION_COPY_NONATOMIC = 3 56 | // swiftlint:disable:next identifier_name 57 | case OBJC_ASSOCIATION_RETAIN = 769 58 | // swiftlint:disable:next identifier_name 59 | case OBJC_ASSOCIATION_COPY = 771 60 | } 61 | func objc_setAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, 62 | _ value: Any?, _ policy: objc_AssociationPolicy) {} 63 | func objc_getAssociatedObject(_ object: Any, 64 | _ key: UnsafeRawPointer) -> Any? { return nil } 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/InterposeKit/ObjectHook.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Interpose { 4 | 5 | /// A hook to an instance method of a single object, stores both the original and new implementation. 6 | /// Think about: Multiple hooks for one object 7 | final public class ObjectHook: TypedHook { 8 | 9 | /// The object that is being hooked. 10 | public let object: AnyObject 11 | 12 | /// Subclass that we create on the fly 13 | var interposeSubclass: InterposeSubclass? 14 | 15 | // Logic switch to use super builder 16 | let generatesSuperIMP = InterposeSubclass.supportsSuperTrampolines 17 | 18 | /// Initialize a new hook to interpose an instance method. 19 | public init(object: AnyObject, selector: Selector, 20 | implementation: (ObjectHook) -> HookSignature?) throws { 21 | self.object = object 22 | try super.init(class: type(of: object), selector: selector) 23 | let block = implementation(self) as AnyObject 24 | replacementIMP = imp_implementationWithBlock(block) 25 | guard replacementIMP != nil else { 26 | throw InterposeError.unknownError("imp_implementationWithBlock failed for \(block) - slots exceeded?") 27 | } 28 | 29 | // Weakly store reference to hook inside the block of the IMP. 30 | Interpose.storeHook(hook: self, to: block) 31 | } 32 | 33 | // /// Release the hook block if possible. 34 | // public override func cleanup() { 35 | // // remove subclass! 36 | // super.cleanup() 37 | // } 38 | 39 | /// The original implementation of the hook. Might be looked up at runtime. Do not cache this. 40 | public override var original: MethodSignature { 41 | // If we switched implementations, return stored. 42 | if let savedOrigIMP = origIMP { 43 | return unsafeBitCast(savedOrigIMP, to: MethodSignature.self) 44 | } 45 | // Else, perform a dynamic lookup 46 | guard let origIMP = lookupOrigIMP else { InterposeError.nonExistingImplementation(`class`, selector).log() 47 | preconditionFailure("IMP must be found for call") 48 | } 49 | return origIMP 50 | } 51 | 52 | /// We look for the parent IMP dynamically, so later modifications to the class are no problem. 53 | private var lookupOrigIMP: MethodSignature? { 54 | var currentClass: AnyClass? = self.class 55 | repeat { 56 | if let currentClass = currentClass, 57 | let method = class_getInstanceMethod(currentClass, self.selector) { 58 | let origIMP = method_getImplementation(method) 59 | return unsafeBitCast(origIMP, to: MethodSignature.self) 60 | } 61 | currentClass = class_getSuperclass(currentClass) 62 | } while currentClass != nil 63 | return nil 64 | } 65 | 66 | /// Looks for an instance method in the exact class, without looking up the hierarchy. 67 | func exactClassImplementsSelector(_ klass: AnyClass, _ selector: Selector) -> Bool { 68 | var methodCount: CUnsignedInt = 0 69 | guard let methodsInAClass = class_copyMethodList(klass, &methodCount) else { return false } 70 | defer { free(methodsInAClass) } 71 | for index in 0 ..< Int(methodCount) { 72 | let method = methodsInAClass[index] 73 | if method_getName(method) == selector { 74 | return true 75 | } 76 | } 77 | return false 78 | } 79 | 80 | var dynamicSubclass: AnyClass { 81 | interposeSubclass!.dynamicClass 82 | } 83 | 84 | override func replaceImplementation() throws { 85 | let method = try validate() 86 | 87 | // Check if there's an existing subclass we can reuse. 88 | // Create one at runtime if there is none. 89 | interposeSubclass = try InterposeSubclass(object: object) 90 | 91 | // The implementation of the call that is hooked must exist. 92 | guard lookupOrigIMP != nil else { 93 | throw InterposeError.nonExistingImplementation(`class`, selector).log() 94 | } 95 | 96 | // This function searches superclasses for implementations 97 | let hasExistingMethod = exactClassImplementsSelector(dynamicSubclass, selector) 98 | let encoding = method_getTypeEncoding(method) 99 | 100 | if self.generatesSuperIMP { 101 | // If the subclass is empty, we create a super trampoline first. 102 | // If a hook already exists, we must skip this. 103 | if !hasExistingMethod { 104 | interposeSubclass!.addSuperTrampoline(selector: selector) 105 | } 106 | 107 | // Replace IMP (by now we guarantee that it exists) 108 | origIMP = class_replaceMethod(dynamicSubclass, selector, replacementIMP, encoding) 109 | guard origIMP != nil else { 110 | throw InterposeError.nonExistingImplementation(dynamicSubclass, selector) 111 | } 112 | Interpose.log("Added -[\(`class`).\(selector)] IMP: \(origIMP!) -> \(replacementIMP!)") 113 | } else { 114 | // Could potentially be unified in the code paths 115 | if hasExistingMethod { 116 | origIMP = class_replaceMethod(dynamicSubclass, selector, replacementIMP, encoding) 117 | if origIMP != nil { 118 | Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!) via replacement") 119 | } else { 120 | Interpose.log("Unable to replace: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") 121 | throw InterposeError.unableToAddMethod(`class`, selector) 122 | } 123 | } else { 124 | let didAddMethod = class_addMethod(dynamicSubclass, selector, replacementIMP, encoding) 125 | if didAddMethod { 126 | Interpose.log("Added -[\(`class`).\(selector)] IMP: \(replacementIMP!)") 127 | } else { 128 | Interpose.log("Unable to add: -[\(`class`).\(selector)] IMP: \(replacementIMP!)") 129 | throw InterposeError.unableToAddMethod(`class`, selector) 130 | } 131 | } 132 | } 133 | } 134 | 135 | override func resetImplementation() throws { 136 | let method = try validate(expectedState: .interposed) 137 | 138 | guard super.origIMP != nil else { 139 | // Removing methods at runtime is not supported. 140 | // https://stackoverflow.com/questions/1315169/ 141 | // how-do-i-remove-instance-methods-at-runtime-in-objective-c-2-0 142 | // 143 | // This codepath will be hit if the super helper is missing. 144 | // We could recreate the whole class at runtime and rebuild all hooks, 145 | // but that seesm excessive when we have a trampoline at our disposal. 146 | Interpose.log("Reset of -[\(`class`).\(selector)] not supported. No IMP") 147 | throw InterposeError.resetUnsupported("No Original IMP found. SuperBuilder missing?") 148 | } 149 | 150 | guard let currentIMP = class_getMethodImplementation(dynamicSubclass, selector) else { 151 | throw InterposeError.unknownError("No Implementation found") 152 | } 153 | 154 | // We are the topmost hook, replace method. 155 | if currentIMP == replacementIMP { 156 | let previousIMP = class_replaceMethod( 157 | dynamicSubclass, selector, origIMP!, method_getTypeEncoding(method)) 158 | guard previousIMP == replacementIMP else { 159 | throw InterposeError.unexpectedImplementation(dynamicSubclass, selector, previousIMP) 160 | } 161 | Interpose.log("Restored -[\(`class`).\(selector)] IMP: \(origIMP!)") 162 | } else { 163 | let nextHook = Interpose.findNextHook(selfHook: self, topmostIMP: currentIMP) 164 | // Replace next's original IMP 165 | nextHook?.origIMP = self.origIMP 166 | } 167 | 168 | // FUTURE: remove class pair! 169 | // This might fail if we get KVO observed. 170 | // objc_disposeClassPair does not return a bool but logs if it fails. 171 | // 172 | // objc_disposeClassPair(dynamicSubclass) 173 | // self.dynamicSubclass = nil 174 | } 175 | } 176 | } 177 | 178 | #if DEBUG 179 | extension Interpose.ObjectHook: CustomDebugStringConvertible { 180 | public var debugDescription: String { 181 | return "\(selector) of \(object) -> \(String(describing: original))" 182 | } 183 | } 184 | #endif 185 | -------------------------------------------------------------------------------- /Sources/InterposeKit/Watcher.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !os(Linux) 4 | import MachO.dyld 5 | #endif 6 | 7 | // MARK: Interpose Class Load Watcher 8 | 9 | extension Interpose { 10 | // Separate definitions to have more eleveant calling syntax when completion is not needed. 11 | 12 | /// Interpose a class once available. Class is passed via `classParts` string array. 13 | @discardableResult public class func whenAvailable(_ classParts: [String], 14 | builder: @escaping (Interpose) throws -> Void) throws -> Waiter { 15 | try whenAvailable(classParts, builder: builder, completion: nil) 16 | } 17 | 18 | /// Interpose a class once available. Class is passed via `classParts` string array, with completion handler. 19 | @discardableResult public class func whenAvailable(_ classParts: [String], 20 | builder: @escaping (Interpose) throws -> Void, 21 | completion: (() -> Void)? = nil) throws -> Waiter { 22 | try whenAvailable(classParts.joined(), builder: builder, completion: completion) 23 | } 24 | 25 | /// Interpose a class once available. Class is passed via `className` string. 26 | @discardableResult public class func whenAvailable(_ className: String, 27 | builder: @escaping (Interpose) throws -> Void) throws -> Waiter { 28 | try whenAvailable(className, builder: builder, completion: nil) 29 | } 30 | 31 | /// Interpose a class once available. Class is passed via `className` string, with completion handler. 32 | @discardableResult public class func whenAvailable(_ className: String, 33 | builder: @escaping (Interpose) throws -> Void, 34 | completion: (() -> Void)? = nil) throws -> Waiter { 35 | try Waiter(className: className, builder: builder, completion: completion) 36 | } 37 | 38 | /// Helper that stores hooks to a specific class and executes them once the class becomes available. 39 | public struct Waiter { 40 | fileprivate let className: String 41 | private var builder: ((Interpose) throws -> Void)? 42 | private var completion: (() -> Void)? 43 | 44 | /// Initialize waiter object. 45 | @discardableResult init(className: String, 46 | builder: @escaping (Interpose) throws -> Void, 47 | completion: (() -> Void)? = nil) throws { 48 | self.className = className 49 | self.builder = builder 50 | self.completion = completion 51 | 52 | // Immediately try to execute task. If not there, install waiter. 53 | if try tryExecute() == false { 54 | InterposeWatcher.append(waiter: self) 55 | } 56 | } 57 | 58 | func tryExecute() throws -> Bool { 59 | guard let `class` = NSClassFromString(className), let builder = self.builder else { return false } 60 | try Interpose(`class`).apply(builder) 61 | if let completion = self.completion { 62 | completion() 63 | } 64 | return true 65 | } 66 | } 67 | } 68 | 69 | // dyld C function cannot capture class context so we pack it in a static struct. 70 | private struct InterposeWatcher { 71 | // Global list of waiters; can be multiple per class 72 | private static var globalWatchers: [Interpose.Waiter] = { 73 | // Register after Swift global registers to not deadlock 74 | DispatchQueue.main.async { InterposeWatcher.installGlobalImageLoadWatcher() } 75 | return [] 76 | }() 77 | 78 | fileprivate static func append(waiter: Interpose.Waiter) { 79 | InterposeWatcher.globalWatcherQueue.sync { 80 | globalWatchers.append(waiter) 81 | } 82 | } 83 | 84 | // Register hook when dyld loads an image 85 | private static let globalWatcherQueue = DispatchQueue(label: "com.steipete.global-image-watcher") 86 | private static func installGlobalImageLoadWatcher() { 87 | _dyld_register_func_for_add_image { _, _ in 88 | InterposeWatcher.globalWatcherQueue.sync { 89 | // this is called on the thread the image is loaded. 90 | InterposeWatcher.globalWatchers = InterposeWatcher.globalWatchers.filter { waiter -> Bool in 91 | do { 92 | if try waiter.tryExecute() == false { 93 | return true // only collect if this fails because class is not there yet 94 | } else { 95 | Interpose.log("\(waiter.className) was successful.") 96 | } 97 | } catch { 98 | Interpose.log("Error while executing task: \(error).") 99 | // We can't bubble up the throw into the C context. 100 | #if DEBUG 101 | // Instead of silently eating, it's better to crash in DEBUG. 102 | fatalError("Error while executing task: \(error).") 103 | #endif 104 | } 105 | return false 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/SuperBuilder/include/ITKSuperBuilder.h: -------------------------------------------------------------------------------- 1 | #if __APPLE__ 2 | #import 3 | #endif 4 | 5 | NS_ASSUME_NONNULL_BEGIN 6 | 7 | /** 8 | Adds an empty super implementation instance method to originalClass. 9 | If a method already exists, this will return NO and a descriptive error message. 10 | 11 | Example: You have an empty UIViewController subclass and call this with viewDidLoad as selector. 12 | The result will be code that looks similar to this: 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | } 17 | 18 | What the compiler creates in following code: 19 | 20 | - (void)viewDidLoad { 21 | struct objc_super _super = { 22 | .receiver = self, 23 | .super_class = object_getClass(obj); 24 | }; 25 | objc_msgSendSuper2(&_super, _cmd); 26 | } 27 | 28 | There are a few important details: 29 | 30 | 1) We use objc_msgSendSuper2, not objc_msgSendSuper. 31 | The difference is minor, but important. 32 | objc_msgSendSuper starts looking at the current class, which would cause an endless loop 33 | objc_msgSendSuper2 looks for the superclass. 34 | 35 | 2) This uses a completely dynamic lookup. 36 | While slightly slower, this is resilient even if you change superclasses later on. 37 | 38 | 3) The resolution method calls out to C, so it could be customized to jump over specific implementations. 39 | (Such API is not currently exposed) 40 | 41 | 4) This uses inline assembly to forward the parameters to objc_msgSendSuper2 and objc_msgSendSuper2_stret. 42 | This is currently implemented architectures are x86_64 and arm64. 43 | armv7 was dropped in OS 11 and i386 with macOS Catalina. 44 | 45 | @see https://steipete.com/posts/calling-super-at-runtime/ 46 | */ 47 | @interface SuperBuilder : NSObject 48 | 49 | /// Adds an empty super implementation instance method to originalClass. 50 | /// If a method already exists, this will return NO and a descriptive error message. 51 | + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error; 52 | 53 | /// Check if the instance method in `originalClass` is a super trampoline. 54 | + (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector; 55 | 56 | /// x86-64 and ARM64 are currently supported. 57 | @property(class, readonly) BOOL isSupportedArchitecure; 58 | 59 | #if (defined (__arm64__) || defined (__x86_64__)) && __APPLE__ 60 | /// Helper that does not exist if architecture is not supported. 61 | + (BOOL)isCompileTimeSupportedArchitecure; 62 | #endif 63 | 64 | @end 65 | 66 | NSString *const SuperBuilderErrorDomain; 67 | 68 | typedef NS_ERROR_ENUM(SuperBuilderErrorDomain, SuperBuilderErrorCode) { 69 | SuperBuilderErrorCodeArchitectureNotSupported, 70 | SuperBuilderErrorCodeNoSuperClass, 71 | SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, 72 | SuperBuilderErrorCodeFailedToAddMethod 73 | }; 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /Sources/SuperBuilder/src/ITKSuperBuilder.m: -------------------------------------------------------------------------------- 1 | #if __APPLE__ 2 | #import "ITKSuperBuilder.h" 3 | 4 | @import ObjectiveC.message; 5 | @import ObjectiveC.runtime; 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | NSString *const SuperBuilderErrorDomain = @"com.steipete.superbuilder"; 10 | 11 | void msgSendSuperTrampoline(void); 12 | void msgSendSuperStretTrampoline(void); 13 | 14 | #define let const __auto_type 15 | #define var __auto_type 16 | 17 | static IMP ITKGetTrampolineForTypeEncoding(__unused const char *typeEncoding) { 18 | BOOL requiresStructDispatch = NO; 19 | #if defined (__arm64__) 20 | // ARM64 doesn't use stret dispatch. Yay! 21 | #elif defined (__x86_64__) 22 | // On x86-64, stret dispatch is ~used whenever return type doesn't fit into two registers 23 | // 24 | // http://www.sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html 25 | // x86_64 is more complicated, including rules for returning floating-point struct fields in FPU registers, and ppc64's rules and exceptions will make your head spin. The gory details are documented in the Mac OS X ABI Guide, though as usual if the documentation and the compiler disagree then the documentation is wrong. 26 | NSUInteger returnTypeActualSize = 0; 27 | NSGetSizeAndAlignment(typeEncoding, &returnTypeActualSize, NULL); 28 | requiresStructDispatch = returnTypeActualSize > (sizeof(void *) * 2); 29 | #else 30 | // Unknown architecture 31 | // https://devblogs.microsoft.com/xamarin/apple-new-processor-architecture/ 32 | // watchOS uses arm64_32 since series 4, before armv7k. watch Simulator uses i386. 33 | // See ILP32: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0490a/ar01s01.html 34 | #endif 35 | 36 | return requiresStructDispatch ? (IMP)msgSendSuperStretTrampoline : (IMP)msgSendSuperTrampoline; 37 | } 38 | 39 | // Helper for binding with Swift 40 | BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error); 41 | BOOL IKTAddSuperImplementationToClass(Class originalClass, SEL selector, NSError **error) { 42 | return [SuperBuilder addSuperInstanceMethodToClass:originalClass selector:selector error:error]; 43 | } 44 | 45 | #define ERROR_AND_RETURN(CODE, STRING)\ 46 | if (error) { *error = [NSError errorWithDomain:SuperBuilderErrorDomain code:CODE userInfo:@{NSLocalizedDescriptionKey: STRING}];} return NO; 47 | 48 | @implementation SuperBuilder 49 | 50 | + (BOOL)isSupportedArchitecure { 51 | #if defined (__arm64__) || defined (__x86_64__) 52 | return YES; 53 | #else 54 | return NO; 55 | #endif 56 | } 57 | 58 | #if defined (__arm64__) || defined (__x86_64__) 59 | + (BOOL)isCompileTimeSupportedArchitecure { 60 | return [self isSupportedArchitecure]; 61 | } 62 | #endif 63 | 64 | + (BOOL)isSuperTrampolineForClass:(Class)originalClass selector:(SEL)selector { 65 | // No architecture check needed - will just be NO. 66 | let method = class_getInstanceMethod(originalClass, selector); 67 | return ITKMethodIsSuperTrampoline(method); 68 | } 69 | 70 | + (BOOL)addSuperInstanceMethodToClass:(Class)originalClass selector:(SEL)selector error:(NSError **)error { 71 | if (!self.isSupportedArchitecure) { 72 | let msg = @"Unsupported Architecture. (Support includes ARM64 and x86-64 )"; 73 | ERROR_AND_RETURN(SuperBuilderErrorCodeArchitectureNotSupported, msg) 74 | } 75 | 76 | // Check that class has a superclass 77 | let superClass = class_getSuperclass(originalClass); 78 | if (superClass == nil) { 79 | let msg = [NSString stringWithFormat:@"Unable to find superclass for %@", NSStringFromClass(originalClass)]; 80 | ERROR_AND_RETURN(SuperBuilderErrorCodeNoSuperClass, msg) 81 | } 82 | 83 | // Fetch method called with super 84 | let method = class_getInstanceMethod(superClass, selector); 85 | if (method == NULL) { 86 | let msg = [NSString stringWithFormat:@"No dynamically dispatched method with selector %@ is available on any of the superclasses of %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; 87 | ERROR_AND_RETURN(SuperBuilderErrorCodeNoDynamicallyDispatchedMethodAvailable, msg) 88 | } 89 | 90 | // Add trampoline 91 | let typeEncoding = method_getTypeEncoding(method); 92 | let trampoline = ITKGetTrampolineForTypeEncoding(typeEncoding); 93 | let methodAdded = class_addMethod(originalClass, selector, trampoline, typeEncoding); 94 | if (!methodAdded) { 95 | let msg = [NSString stringWithFormat:@"Failed to add method for selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(originalClass)]; 96 | ERROR_AND_RETURN(SuperBuilderErrorCodeFailedToAddMethod, msg) 97 | } 98 | return methodAdded; 99 | } 100 | 101 | // Control if the trampoline should also push/pop the floating point registers. 102 | // This is slightly slower and not needed for our simple implementation 103 | // However, even if you just use memcpy, you will want to enable this. 104 | // We keep this enabled to be doubly safe. 105 | #define PROTECT_FLOATING_POINT_REGISTERS 1 106 | 107 | // One thread local per thread should be enough 108 | _Thread_local struct objc_super _threadSuperStorage; 109 | 110 | static BOOL ITKMethodIsSuperTrampoline(Method method) { 111 | let methodIMP = method_getImplementation(method); 112 | return methodIMP == (IMP)msgSendSuperTrampoline || methodIMP == (IMP)msgSendSuperStretTrampoline; 113 | } 114 | 115 | struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd); 116 | struct objc_super *ITKReturnThreadSuper(__unsafe_unretained id obj, SEL _cmd) { 117 | /** 118 | Assume you have a class hierarchy made of four classes `Level1` <- `Level2` <- `Level3` <- `Level4`, 119 | with `Level1` implementing a method called `-sayHello`, not implemented elsewhere in descendants classes. 120 | 121 | If you use: `[SuperBuilder addSuperInstanceMethodToClass:Level2.class selector:@selector(sayHello) error:NULL];` 122 | to inject a _dummy_ implementation at `Level2`, the following will happen: 123 | 124 | - Calling `-[Level2 sayHello]` works. The trampoline is called, the `super_class ` is found to be `Level1`, and the `-sayHello` parent implementation is called. 125 | - Calling `-[LevelN sayHello]` for any N > 2 ends in an infinite recursion. Since the `obj` passed to the trampoline is a descendant of `Level2`, `objc_msgSendSuper2` will of course call the injected implementation on `Level2`, which in turn will call itself with the same arguments, again and again. 126 | 127 | This is fixed by walking up the hierarchy until we find the class implementing the method. 128 | 129 | Looking at the method implementation we can also skip subsequent super calls. 130 | */ 131 | Class clazz = object_getClass(obj); 132 | Class superclazz = class_getSuperclass(clazz); 133 | do { 134 | let superclassMethod = class_getInstanceMethod(superclazz, _cmd); 135 | let sameMethods = class_getInstanceMethod(clazz, _cmd) == superclassMethod; 136 | if (!sameMethods && !ITKMethodIsSuperTrampoline(superclassMethod)) { 137 | break; 138 | } 139 | clazz = superclazz; 140 | superclazz = class_getSuperclass(clazz); 141 | }while (1); 142 | 143 | struct objc_super *_super = &_threadSuperStorage; 144 | _super->receiver = obj; 145 | _super->super_class = clazz; 146 | return _super; 147 | } 148 | 149 | @end 150 | 151 | /** 152 | Inline assembly is used to perfectly forward all parameters to objc_msgSendSuper, 153 | while also looking up the target on-the-fly. 154 | 155 | Assembly is hard, here are some useful resources: 156 | 157 | https://azeria-labs.com/functions-and-the-stack-part-7/ 158 | https://github.com/DavidGoldman/InspectiveC/blob/master/InspectiveCarm64.mm 159 | https://blog.nelhage.com/2010/10/amd64-and-va_arg/ 160 | https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html 161 | https://c9x.me/compile/bib/abi-arm64.pdf 162 | http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0801a/BABBDBAD.html 163 | https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/using-the-stack-in-aarch64-implementing-push-and-pop 164 | https://www.cs.yale.edu/flint/cs421/papers/x86-asm/asm.html 165 | https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64 166 | https://en.wikipedia.org/wiki/Calling_convention#x86_(32-bit) 167 | https://bob.cs.sonoma.edu/IntroCompOrg-RPi/sec-varstack.html 168 | https://azeria-labs.com/functions-and-the-stack-part-7/ 169 | */ 170 | 171 | #if defined(__arm64__) 172 | 173 | __attribute__((__naked__)) 174 | void msgSendSuperTrampoline(void) { 175 | asm volatile ( 176 | 177 | #if PROTECT_FLOATING_POINT_REGISTERS 178 | // push {q0-q7} floating point registers 179 | "stp q6, q7, [sp, #-32]!\n" 180 | "stp q4, q5, [sp, #-32]!\n" 181 | "stp q2, q3, [sp, #-32]!\n" 182 | "stp q0, q1, [sp, #-32]!\n" 183 | #endif 184 | 185 | // push {x0-x8, lr} (call params are: x0-x7) 186 | // stp: store pair of registers: from, from, to, via indexed write 187 | "stp x8, lr, [sp, #-16]!\n" // push lr (link register == x30), then x8 188 | "stp x6, x7, [sp, #-16]!\n" 189 | "stp x4, x5, [sp, #-16]!\n" 190 | "stp x2, x3, [sp, #-16]!\n" // push x3, then x2 191 | "stp x0, x1, [sp, #-16]!\n" // push x1, then x0 192 | 193 | // fetch filled struct objc_super, call with self + _cmd 194 | "bl _ITKReturnThreadSuper \n" 195 | 196 | // first param is now struct objc_super (x0) 197 | // protect returned new value when we restore the pairs 198 | "mov x9, x0\n" 199 | 200 | // pop {x0-x8, lr} 201 | "ldp x0, x1, [sp], #16\n" 202 | "ldp x2, x3, [sp], #16\n" 203 | "ldp x4, x5, [sp], #16\n" 204 | "ldp x6, x7, [sp], #16\n" 205 | "ldp x8, lr, [sp], #16\n" 206 | 207 | #if PROTECT_FLOATING_POINT_REGISTERS 208 | // pop {q0-q7} 209 | "ldp q6, q7, [sp], #32\n" 210 | "ldp q4, q5, [sp], #32\n" 211 | "ldp q2, q3, [sp], #32\n" 212 | "ldp q0, q1, [sp], #32\n" 213 | #endif 214 | 215 | // get new return (adr of the objc_super class) 216 | "mov x0, x9\n" 217 | // tail call 218 | "b _objc_msgSendSuper2 \n" 219 | : : : "x0", "x1"); 220 | } 221 | 222 | // arm64 doesn't use _stret variants. 223 | void msgSendSuperStretTrampoline(void) {} 224 | 225 | #elif defined(__x86_64__) 226 | 227 | __attribute__((__naked__)) 228 | void msgSendSuperTrampoline(void) { 229 | asm volatile ( 230 | // push frame pointer 231 | "pushq %%rbp \n" 232 | // set stack to frame pointer 233 | "movq %%rsp, %%rbp \n" 234 | 235 | #if PROTECT_FLOATING_POINT_REGISTERS 236 | // reserve 48+4*16 = 112 byte on the stack (need 16 byte alignment) 237 | "subq $112, %%rsp \n" 238 | 239 | "movdqu %%xmm0, -64(%%rbp) \n" 240 | "movdqu %%xmm1, -80(%%rbp) \n" 241 | "movdqu %%xmm2, -96(%%rbp) \n" 242 | "movdqu %%xmm3, -112(%%rbp) \n" 243 | #else 244 | // reserve 48 byte on the stack (need 16 byte alignment) 245 | "subq $48, %%rsp \n" 246 | #endif 247 | 248 | // Save call params: rdi, rsi, rdx, rcx, r8, r9 249 | // 250 | // First parameter can be avoided, 251 | // but we need to keep the stack 16-byte algined. 252 | //"movq %%rdi, -8(%%rbp) \n" // self po *(id *) 253 | "movq %%rsi, -16(%%rbp) \n" // _cmd p (SEL)$rsi 254 | "movq %%rdx, -24(%%rbp) \n" // param 1 255 | "movq %%rcx, -32(%%rbp) \n" // param 2 256 | "movq %%r8, -40(%%rbp) \n" // param 3 257 | "movq %%r9, -48(%%rbp) \n" // param 4 (rest goes on stack) 258 | 259 | // fetch filled struct objc_super, call with self + _cmd 260 | "callq _ITKReturnThreadSuper \n" 261 | // first param is now struct objc_super 262 | "movq %%rax, %%rdi \n" 263 | 264 | #if PROTECT_FLOATING_POINT_REGISTERS 265 | "movdqu -64(%%rbp), %%xmm0 \n" 266 | "movdqu -80(%%rbp), %%xmm1 \n" 267 | "movdqu -96(%%rbp), %%xmm2 \n" 268 | "movdqu -112(%%rbp), %%xmm3 \n" 269 | #endif 270 | 271 | // Restore call params 272 | // do not restore first parameter: super class 273 | "movq -16(%%rbp), %%rsi \n" 274 | "movq -24(%%rbp), %%rdx \n" 275 | "movq -32(%%rbp), %%rcx \n" 276 | "movq -40(%%rbp), %%r8 \n" 277 | "movq -48(%%rbp), %%r9 \n" 278 | 279 | // debug stack via print *(int *) ($rsp+8) 280 | // remove 112/48 byte from stack 281 | #if PROTECT_FLOATING_POINT_REGISTERS 282 | "addq $112, %%rsp \n" 283 | #else 284 | "addq $48, %%rsp \n" 285 | #endif 286 | // pop frame pointer 287 | "popq %%rbp \n" 288 | 289 | // tail call time! 290 | "jmp _objc_msgSendSuper2 \n" 291 | : : : "rsi", "rdi"); 292 | } 293 | 294 | 295 | __attribute__((__naked__)) 296 | void msgSendSuperStretTrampoline(void) { 297 | asm volatile ( 298 | // push frame pointer 299 | "pushq %%rbp \n" 300 | // set stack to frame pointer 301 | "movq %%rsp, %%rbp \n" 302 | // reserve 48 byte on the stack (need 16 byte alignment) 303 | "subq $48, %%rsp \n" 304 | 305 | // Save call params: rdi, rsi, rdx, rcx, r8, r9 306 | "movq %%rdi, -8(%%rbp) \n" // struct return 307 | "movq %%rsi, -16(%%rbp) \n" // self 308 | "movq %%rdx, -24(%%rbp) \n" // _cmd 309 | "movq %%rcx, -32(%%rbp) \n" // param 1 310 | "movq %%r8, -40(%%rbp) \n" // param 2 311 | "movq %%r9, -48(%%rbp) \n" // param 3 (rest goes on stack) 312 | 313 | // fetch filled struct objc_super, call with self + _cmd 314 | // Since stret offsets, we move back by one 315 | "movq -16(%%rbp), %%rdi \n" 316 | "movq -24(%%rbp), %%rsi \n" 317 | "callq _ITKReturnThreadSuper \n" 318 | // second param is now struct objc_super 319 | "movq %%rax, %%rsi \n" 320 | // First is our struct return 321 | 322 | // Restore call params 323 | "movq -8(%%rbp), %%rdi \n" 324 | // do not restore second parameter: super class 325 | "movq -24(%%rbp), %%rdx \n" 326 | "movq -32(%%rbp), %%rcx \n" 327 | "movq -40(%%rbp), %%r8 \n" 328 | "movq -48(%%rbp), %%r9 \n" 329 | 330 | // debug stack via print *(int *) ($rsp+8) 331 | // remove 64 byte from stack 332 | "addq $48, %%rsp \n" 333 | // pop frame pointer 334 | "popq %%rbp \n" 335 | 336 | // tail call time! 337 | "jmp _objc_msgSendSuper2_stret \n" 338 | : : : "rsi", "rdi"); 339 | } 340 | 341 | #else 342 | // Unknown architecture - time to write some assembly :) 343 | void msgSendSuperTrampoline(void) {} 344 | void msgSendSuperStretTrampoline(void) {} 345 | #endif 346 | 347 | NS_ASSUME_NONNULL_END 348 | #endif 349 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/InterposeKitTestCase.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import InterposeKit 3 | 4 | class InterposeKitTestCase: XCTestCase { 5 | override func setUpWithError() throws { 6 | Interpose.isLoggingEnabled = true 7 | } 8 | } 9 | 10 | extension InterposeKitTestCase { 11 | /// Assert that a specific error is thrown. 12 | func assert( 13 | _ expression: @autoclosure () throws -> T, 14 | throws error: E, 15 | in file: StaticString = #file, 16 | line: UInt = #line 17 | ) { 18 | // https://www.swiftbysundell.com/articles/testing-error-code-paths-in-swift/ 19 | var thrownError: Error? 20 | 21 | XCTAssertThrowsError(try expression(), 22 | file: file, line: line) { 23 | thrownError = $0 24 | } 25 | 26 | XCTAssertTrue( 27 | thrownError is E, 28 | "Unexpected error type: \(type(of: thrownError))", 29 | file: file, line: line 30 | ) 31 | 32 | XCTAssertEqual( 33 | thrownError as? E, error, 34 | file: file, line: line 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/InterposeKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import InterposeKit 3 | 4 | final class InterposeKitTests: InterposeKitTestCase { 5 | 6 | override func setUpWithError() throws { 7 | Interpose.isLoggingEnabled = true 8 | } 9 | 10 | func testClassOverrideAndRevert() throws { 11 | let testObj = TestClass() 12 | XCTAssertEqual(testObj.sayHi(), testClassHi) 13 | 14 | // Functions need to be `@objc dynamic` to be hookable. 15 | let interposer = try Interpose(TestClass.self) { 16 | try $0.prepareHook( 17 | #selector(TestClass.sayHi), 18 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 19 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 20 | // You're free to skip calling the original implementation. 21 | print("Before Interposing \(bSelf)") 22 | let string = store.original(bSelf, store.selector) 23 | print("After Interposing \(bSelf)") 24 | 25 | return string + testString 26 | } 27 | } 28 | } 29 | print(TestClass().sayHi()) 30 | 31 | // Test various apply/revert's 32 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString) 33 | try interposer.revert() 34 | XCTAssertEqual(testObj.sayHi(), testClassHi) 35 | try interposer.apply() 36 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString) 37 | XCTAssertThrowsError(try interposer.apply()) 38 | XCTAssertThrowsError(try interposer.apply()) 39 | try interposer.revert() 40 | XCTAssertThrowsError(try interposer.revert()) 41 | try interposer.apply() 42 | try interposer.revert() 43 | XCTAssertEqual(testObj.sayHi(), testClassHi) 44 | } 45 | 46 | func testSubclassOverride() throws { 47 | let testObj = TestSubclass() 48 | XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) 49 | 50 | // Swizzle test class 51 | let interposed = try Interpose(TestClass.self) { 52 | try $0.prepareHook( 53 | #selector(TestClass.sayHi), 54 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 55 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 56 | return store.original(bSelf, store.selector) + testString 57 | } 58 | } 59 | } 60 | 61 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) 62 | try interposed.revert() 63 | XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) 64 | try interposed.apply() 65 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass) 66 | 67 | // Swizzle subclass, automatically applys 68 | let interposedSubclass = try Interpose(TestSubclass.self).hook( 69 | #selector(TestSubclass.sayHi), 70 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 71 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 72 | return store.original(bSelf, store.selector) + testString 73 | } 74 | } 75 | 76 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testSubclass + testString) 77 | try interposed.revert() 78 | XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass + testString) 79 | try interposedSubclass.revert() 80 | XCTAssertEqual(testObj.sayHi(), testClassHi + testSubclass) 81 | } 82 | 83 | func testInterposedCleanup() throws { 84 | var deallocated = false 85 | 86 | try autoreleasepool { 87 | let tracker = LifetimeTracker { 88 | deallocated = true 89 | } 90 | 91 | // Swizzle test class 92 | let interposer = try Interpose(TestClass.self).hook( 93 | #selector(TestClass.doNothing), 94 | methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, 95 | hookSignature: (@convention(block) (AnyObject) -> Void).self) { store in { bSelf in 96 | tracker.keep() 97 | return store.original(bSelf, store.selector) 98 | } 99 | } 100 | 101 | // Dealloc interposer without removing hooks 102 | _ = interposer 103 | } 104 | 105 | // Unreverted block should not be deallocated 106 | XCTAssertFalse(deallocated) 107 | } 108 | 109 | func testRevertedCleanup() throws { 110 | var deallocated = false 111 | 112 | try autoreleasepool { 113 | let tracker = LifetimeTracker { 114 | deallocated = true 115 | } 116 | 117 | // Swizzle test class 118 | let interposer = try Interpose(TestClass.self) { 119 | try $0.prepareHook( 120 | #selector(TestClass.doNothing), 121 | methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self, 122 | hookSignature: (@convention(block) (AnyObject) -> Void).self) { store in { bSelf in 123 | tracker.keep() 124 | return store.original(bSelf, store.selector) 125 | } 126 | } 127 | } 128 | try interposer.revert() 129 | } 130 | 131 | // Verify that the block was deallocated 132 | XCTAssertTrue(deallocated) 133 | } 134 | 135 | func testImpRemoveBlockWorks() { 136 | var deallocated = false 137 | 138 | let imp: IMP = autoreleasepool { 139 | let tracker = LifetimeTracker { 140 | deallocated = true 141 | } 142 | 143 | let block: @convention(block) (AnyObject) -> Void = { _ in 144 | // retain `tracker` inside a block 145 | tracker.keep() 146 | } 147 | 148 | return imp_implementationWithBlock(block) 149 | } 150 | 151 | // `imp` retains `block` which retains `tracker` 152 | XCTAssertFalse(deallocated) 153 | 154 | // Detach `block` from `imp` 155 | imp_removeBlock(imp) 156 | 157 | // `block` and `tracker` should be deallocated now 158 | XCTAssertTrue(deallocated) 159 | } 160 | 161 | class LifetimeTracker { 162 | let deinitCalled: () -> Void 163 | 164 | init(deinitCalled: @escaping () -> Void) { 165 | self.deinitCalled = deinitCalled 166 | } 167 | 168 | deinit { 169 | deinitCalled() 170 | } 171 | 172 | func keep() { } 173 | } 174 | 175 | static var allTests = [ 176 | ("testClassOverrideAndRevert", testClassOverrideAndRevert), 177 | ("testSubclassOverride", testSubclassOverride), 178 | ("testInterposedCleanup", testInterposedCleanup), 179 | ("testRevertedCleanup", testRevertedCleanup), 180 | ("testImpRemoveBlockWorks", testImpRemoveBlockWorks) 181 | ] 182 | } 183 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/KVOTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import InterposeKit 4 | 5 | final class KVOTests: InterposeKitTestCase { 6 | 7 | // Helper observer that wraps a token and removes it on deinit. 8 | class TestClassObserver { 9 | var kvoToken: NSKeyValueObservation? 10 | var didCallObserver = false 11 | 12 | func observe(obj: TestClass) { 13 | kvoToken = obj.observe(\.age, options: .new) { [weak self] _, change in 14 | guard let age = change.newValue else { return } 15 | print("New age is: \(age)") 16 | self?.didCallObserver = true 17 | } 18 | } 19 | 20 | deinit { 21 | kvoToken?.invalidate() 22 | } 23 | } 24 | 25 | func testBasicKVO() throws { 26 | let testObj = TestClass() 27 | 28 | // KVO before hooking works, but hooking will fail 29 | try withExtendedLifetime(TestClassObserver()) { observer in 30 | observer.observe(obj: testObj) 31 | XCTAssertEqual(testObj.age, 1) 32 | testObj.age = 2 33 | XCTAssertEqual(testObj.age, 2) 34 | // Hooking is expected to fail 35 | assert(try Interpose(testObj), throws: InterposeError.keyValueObservationDetected(testObj)) 36 | XCTAssertEqual(testObj.age, 2) 37 | } 38 | 39 | // Hook without KVO! 40 | let hook = try testObj.hook( 41 | #selector(getter: TestClass.age), 42 | methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, 43 | hookSignature: (@convention(block) (AnyObject) -> Int).self) { _ in { _ in 44 | return 3 45 | } 46 | } 47 | XCTAssertEqual(testObj.age, 3) 48 | try hook.revert() 49 | XCTAssertEqual(testObj.age, 2) 50 | try hook.apply() 51 | XCTAssertEqual(testObj.age, 3) 52 | 53 | // Now we KVO after hooking! 54 | withExtendedLifetime(TestClassObserver()) { observer in 55 | observer.observe(obj: testObj) 56 | XCTAssertEqual(testObj.age, 3) 57 | // Setter is fine but won't change outcome 58 | XCTAssertFalse(observer.didCallObserver) 59 | testObj.age = 4 60 | XCTAssertTrue(observer.didCallObserver) 61 | XCTAssertEqual(testObj.age, 3) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/MultipleInterposing.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import InterposeKit 4 | 5 | final class MultipleInterposingTests: InterposeKitTestCase { 6 | 7 | func testInterposeSingleObjectMultipleTimes() throws { 8 | let testObj = TestClass() 9 | let testObj2 = TestClass() 10 | 11 | XCTAssertEqual(testObj.sayHi(), testClassHi) 12 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 13 | 14 | // Functions need to be `@objc dynamic` to be hookable. 15 | let interposer = try Interpose(testObj) { 16 | try $0.prepareHook( 17 | #selector(TestClass.sayHi), 18 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 19 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 20 | return store.original(bSelf, store.selector) + testString 21 | } 22 | } 23 | } 24 | 25 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString) 26 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 27 | 28 | try testObj.hook( 29 | #selector(TestClass.sayHi), 30 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 31 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 32 | return store.original(bSelf, store.selector) + testString2 33 | } 34 | } 35 | 36 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString + testString2) 37 | try interposer.revert() 38 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString2) 39 | } 40 | 41 | func testInterposeAgeAndRevert() throws { 42 | let testObj = TestClass() 43 | XCTAssertEqual(testObj.age, 1) 44 | 45 | let interpose = try Interpose(testObj) { 46 | try $0.prepareHook(#selector(getter: TestClass.age), 47 | methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, 48 | hookSignature: (@convention(block) (AnyObject) -> Int).self) { _ in { _ in 49 | return 3 50 | } 51 | } 52 | } 53 | XCTAssertEqual(testObj.age, 3) 54 | 55 | try interpose.hook(#selector(getter: TestClass.age), 56 | methodSignature: (@convention(c) (AnyObject, Selector) -> Int).self, 57 | hookSignature: (@convention(block) (AnyObject) -> Int).self) { _ in { _ in 58 | return 5 59 | } 60 | } 61 | XCTAssertEqual(testObj.age, 5) 62 | try interpose.revert() 63 | XCTAssertEqual(testObj.age, 1) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/ObjectInterposeTests.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import XCTest 3 | @testable import InterposeKit 4 | 5 | final class ObjectInterposeTests: InterposeKitTestCase { 6 | 7 | func testInterposeSingleObject() throws { 8 | let testObj = TestClass() 9 | let testObj2 = TestClass() 10 | 11 | XCTAssertEqual(testObj.sayHi(), testClassHi) 12 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 13 | 14 | let hook = try testObj.hook( 15 | #selector(TestClass.sayHi), 16 | methodSignature: (@convention(c) (AnyObject, Selector) -> String).self, 17 | hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { bSelf in 18 | print("Before Interposing \(bSelf)") 19 | let string = store.original(bSelf, store.selector) 20 | print("After Interposing \(bSelf)") 21 | return string + testString 22 | } 23 | } 24 | 25 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString) 26 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 27 | try hook.revert() 28 | XCTAssertEqual(testObj.sayHi(), testClassHi) 29 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 30 | try hook.apply() 31 | XCTAssertEqual(testObj.sayHi(), testClassHi + testString) 32 | XCTAssertEqual(testObj2.sayHi(), testClassHi) 33 | } 34 | 35 | func testInterposeSingleObjectInt() throws { 36 | let testObj = TestClass() 37 | let returnIntDefault = testObj.returnInt() 38 | let returnIntOverrideOffset = 2 39 | XCTAssertEqual(testObj.returnInt(), returnIntDefault) 40 | 41 | let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook 42 | <@convention(c) (AnyObject, Selector) -> Int, 43 | @convention(block) (AnyObject) -> Int>) in { 44 | let int = store.original($0, store.selector) 45 | return int + returnIntOverrideOffset 46 | } 47 | } 48 | 49 | XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) 50 | try hook.revert() 51 | XCTAssertEqual(testObj.returnInt(), returnIntDefault) 52 | try hook.apply() 53 | // ensure we really don't leak into another object 54 | let testObj2 = TestClass() 55 | XCTAssertEqual(testObj2.returnInt(), returnIntDefault) 56 | XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) 57 | try hook.revert() 58 | XCTAssertEqual(testObj.returnInt(), returnIntDefault) 59 | } 60 | 61 | func testDoubleIntegerInterpose() throws { 62 | let testObj = TestClass() 63 | let returnIntDefault = testObj.returnInt() 64 | let returnIntOverrideOffset = 2 65 | let returnIntClassMultiplier = 4 66 | XCTAssertEqual(testObj.returnInt(), returnIntDefault) 67 | 68 | // Functions need to be `@objc dynamic` to be hookable. 69 | let hook = try testObj.hook(#selector(TestClass.returnInt)) { (store: TypedHook 70 | <@convention(c) (AnyObject, Selector) -> Int, 71 | @convention(block) (AnyObject) -> Int>) in { 72 | // You're free to skip calling the original implementation. 73 | store.original($0, store.selector) + returnIntOverrideOffset 74 | } 75 | } 76 | XCTAssertEqual(testObj.returnInt(), returnIntDefault + returnIntOverrideOffset) 77 | 78 | // Interpose on TestClass itself! 79 | let classInterposer = try Interpose(TestClass.self) { 80 | try $0.prepareHook(#selector(TestClass.returnInt)) { (store: TypedHook 81 | <@convention(c) (AnyObject, Selector) -> Int, 82 | @convention(block) (AnyObject) -> Int>) in { 83 | store.original($0, store.selector) * returnIntClassMultiplier 84 | } 85 | } 86 | } 87 | 88 | XCTAssertEqual(testObj.returnInt(), (returnIntDefault * returnIntClassMultiplier) + returnIntOverrideOffset) 89 | 90 | // ensure we really don't leak into another object 91 | let testObj2 = TestClass() 92 | XCTAssertEqual(testObj2.returnInt(), returnIntDefault * returnIntClassMultiplier) 93 | 94 | try hook.revert() 95 | XCTAssertEqual(testObj.returnInt(), returnIntDefault * returnIntClassMultiplier) 96 | try classInterposer.revert() 97 | XCTAssertEqual(testObj.returnInt(), returnIntDefault) 98 | } 99 | 100 | func test3IntParameters() throws { 101 | let testObj = TestClass() 102 | XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3) 103 | 104 | // Functions need to be `@objc dynamic` to be hookable. 105 | let hook = try testObj.hook(#selector(TestClass.calculate)) { (store: TypedHook 106 | <@convention(c) (AnyObject, Selector, Int, Int, Int) -> Int, 107 | @convention(block) (AnyObject, Int, Int, Int) -> Int>) in { 108 | // You're free to skip calling the original implementation. 109 | let orig = store.original($0, store.selector, $1, $2, $3) 110 | return orig + 1 111 | } 112 | } 113 | XCTAssertEqual(testObj.calculate(var1: 1, var2: 2, var3: 3), 1 + 2 + 3 + 1) 114 | try hook.revert() 115 | } 116 | 117 | func test6IntParameters() throws { 118 | let testObj = TestClass() 119 | 120 | XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, 121 | var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6) 122 | 123 | // Functions need to be `@objc dynamic` to be hookable. 124 | let hook = try testObj.hook(#selector(TestClass.calculate2)) { (store: TypedHook 125 | <@convention(c) (AnyObject, Selector, Int, Int, Int, Int, Int, Int) -> Int, 126 | @convention(block) (AnyObject, Int, Int, Int, Int, Int, Int) -> Int>) in { 127 | // You're free to skip calling the original implementation. 128 | let orig = store.original($0, store.selector, $1, $2, $3, $4, $5, $6) 129 | return orig + 1 130 | } 131 | } 132 | XCTAssertEqual(testObj.calculate2(var1: 1, var2: 2, var3: 3, 133 | var4: 4, var5: 5, var6: 6), 1 + 2 + 3 + 4 + 5 + 6 + 1) 134 | try hook.revert() 135 | } 136 | 137 | func testObjectCallReturn() throws { 138 | let testObj = TestClass() 139 | let str = "foo" 140 | XCTAssertEqual(testObj.doubleString(string: str), str + str) 141 | 142 | // Functions need to be `@objc dynamic` to be hookable. 143 | let hook = try testObj.hook(#selector(TestClass.doubleString)) { (store: TypedHook 144 | <@convention(c) (AnyObject, Selector, String) -> String, 145 | @convention(block) (AnyObject, String) -> String>) in { 146 | store.original($0, store.selector, $1) + str 147 | } 148 | } 149 | XCTAssertEqual(testObj.doubleString(string: str), str + str + str) 150 | try hook.revert() 151 | XCTAssertEqual(testObj.doubleString(string: str), str + str) 152 | } 153 | 154 | func testLargeStructReturn() throws { 155 | let testObj = TestClass() 156 | let transform = CATransform3D() 157 | XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) 158 | 159 | func transformMatrix(_ matrix: CATransform3D) -> CATransform3D { 160 | matrix.translated(x: 10, y: 5, z: 2) 161 | } 162 | 163 | // Functions need to be `@objc dynamic` to be hookable. 164 | let hook = try testObj.hook(#selector(TestClass.invert3DTransform)) { (store: TypedHook 165 | <@convention(c)(AnyObject, Selector, CATransform3D) -> CATransform3D, 166 | @convention(block) (AnyObject, CATransform3D) -> CATransform3D>) in { 167 | let matrix = store.original($0, store.selector, $1) 168 | return transformMatrix(matrix) 169 | } 170 | } 171 | XCTAssertEqual(testObj.invert3DTransform(transform), transformMatrix(transform.inverted)) 172 | try hook.revert() 173 | XCTAssertEqual(testObj.invert3DTransform(transform), transform.inverted) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/TestClass.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import QuartzCore 3 | 4 | let testClassHi = "Hi from TestClass!" 5 | let testString = " and Interpose" 6 | let testString2 = " testString2" 7 | let testSubclass = "Subclass is here!" 8 | 9 | public func == (lhs: CATransform3D, rhs: CATransform3D) -> Bool { 10 | return CATransform3DEqualToTransform(lhs, rhs) 11 | } 12 | 13 | extension CATransform3D: Equatable { } 14 | 15 | public extension CATransform3D { 16 | 17 | // swiftlint:disable:next identifier_name 18 | func translated(x: CGFloat = 0, y: CGFloat = 0, z: CGFloat = 0) -> CATransform3D { 19 | return CATransform3DTranslate(self, x, y, z) 20 | } 21 | 22 | var inverted: CATransform3D { 23 | return CATransform3DInvert(self) 24 | } 25 | } 26 | 27 | class TestClass: NSObject { 28 | 29 | @objc dynamic var age: Int = 1 30 | @objc dynamic var name: String = "Tim Apple" 31 | 32 | @objc dynamic func sayHi() -> String { 33 | print(testClassHi) 34 | return testClassHi 35 | } 36 | 37 | @objc dynamic func doNothing() { } 38 | 39 | @objc dynamic func doubleString(string: String) -> String { 40 | string + string 41 | } 42 | 43 | @objc dynamic func returnInt() -> Int { 44 | 7 45 | } 46 | 47 | @objc dynamic func calculate(var1: Int, var2: Int, var3: Int) -> Int { 48 | var1 + var2 + var3 49 | } 50 | 51 | // swiftlint:disable:next function_parameter_count 52 | @objc dynamic func calculate2(var1: Int, var2: Int, var3: Int, var4: Int, var5: Int, var6: Int) -> Int { 53 | var1 + var2 + var3 + var4 + var5 + var6 54 | } 55 | 56 | // This requires _objc_msgSendSuper_stret on x64, returns a large struct 57 | @objc dynamic func invert3DTransform(_ input: CATransform3D) -> CATransform3D { 58 | input.inverted 59 | } 60 | } 61 | 62 | class TestSubclass: TestClass { 63 | override func sayHi() -> String { 64 | return super.sayHi() + testSubclass 65 | } 66 | 67 | override func doNothing() { 68 | super.doNothing() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/InterposeKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(InterposeKitTests.allTests) 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import InterposeKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += InterposeKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /logo-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steipete/InterposeKit/2d48227c359b71be057fe9fd403ff791e5322b32/logo-social.png -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steipete/InterposeKit/2d48227c359b71be057fe9fd403ff791e5322b32/logo.png --------------------------------------------------------------------------------