├── .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 |
2 |
3 | [](https://github.com/steipete/InterposeKit/actions?query=workflow%3ASwiftPM)
4 | [](https://github.com/steipete/InterposeKit/actions?query=workflow%3Axcodebuild)
5 | [](https://github.com/steipete/InterposeKit/actions?query=workflow%3A%22pod+lib+lint%22)
6 | 
7 | 
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 | [](https://github.com/Carthage/Carthage)
186 | 
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
--------------------------------------------------------------------------------