• Stars
    star
    962
  • Rank 45,906 (Top 1.0 %)
  • Language
    Swift
  • License
    MIT License
  • Created about 4 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A modern library to swizzle elegantly in Swift.

InterposeKit

SwiftPM xcodebuild pod lib lint Xcode 11.4+ Swift 5.2+

InterposeKit is a modern library to swizzle elegantly in Swift, supporting hooks on classes and individual objects. It is well-documented, tested, 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, which required tricky swizzling to fix, I also wrote up implementation thoughts on my blog.

Instead of adding new methods and exchanging implementations based on method_exchangeImplementations, this library replaces the implementation directly using class_replaceMethod. This avoids some of the usual problems with swizzling.

You can call the original implementation and add code before, instead or after a method call.
This is similar to the Aspects library, but doesn't yet do dynamic subclassing.

Compare: Swizzling a property without helper and with InterposeKit

Usage

Let's say you want to amend sayHi from TestClass:

class TestClass: NSObject {
    // Functions need to be marked as `@objc dynamic` or written in Objective-C.
    @objc dynamic func sayHi() -> String {
        print("Calling sayHi")
        return "Hi there 👋"
    }
}

let interposer = try Interpose(TestClass.self) {
    try $0.prepareHook(
        #selector(TestClass.sayHi),
        methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
        hookSignature: (@convention(block) (AnyObject) -> String).self) {
            store in { `self` in
                print("Before Interposing \(`self`)")
                let string = store.original(`self`, store.selector) // free to skip
                print("After Interposing \(`self`)")
                return string + "and Interpose"
            }
    }
}

// Don't need the hook anymore? Undo is built-in!
interposer.revert()

Want to hook just a single instance? No problem!

let hook = try testObj.hook(
    #selector(TestClass.sayHi),
    methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
    hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in
        return store.original(`self`, store.selector) + "just this instance"
        }
}

Here's what we get when calling print(TestClass().sayHi())

[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020
Before Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Calling sayHi
After Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Hi there 👋 and Interpose

Key Features

  • Interpose directly modifies the implementation of a Method, which is safer than selector-based swizzling.
  • Interpose works on classes and individual objects.
  • Hooks can easily be undone via calling revert(). This also checks and errors if someone else changed stuff in between.
  • Mostly Swift, no NSInvocation, which requires boxing and can be slow.
  • No Type checking. If you have a typo or forget a convention part, this will crash at runtime.
  • Yes, you have to type the resulting type twice This is a tradeoff, else we need NSInvocation.
  • Delayed Interposing helps when a class is loaded at runtime. This is useful for Mac Catalyst.

Object Hooking

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.

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.

Various ways to define the signature

Next to using methodSignature and hookSignature, following variants to define the signature are also possible:

methodSignature + casted block

let interposer = try Interpose(testObj) {
    try $0.hook(
        #selector(TestClass.sayHi),
        methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in
            let string = store.original(`self`, store.selector)
            return string + testString
            } as @convention(block) (AnyObject) -> String }
}

Define type via store object

// Functions need to be `@objc dynamic` to be hookable.
let interposer = try Interpose(testObj) {
    try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in {

        // You're free to skip calling the original implementation.
        let int = store.original($0, store.selector)
        return int + returnIntOverrideOffset
        }
    }
}

Delayed Hooking

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.

try Interpose.whenAvailable(["RTIInput", "SystemSession"]) {
    let lock = DispatchQueue(label: "com.steipete.document-state-hack")
    try $0.hook("documentState", { store in { `self` in
        lock.sync {
            store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector)
        }} as @convention(block) (AnyObject) -> AnyObject})

    try $0.hook("setDocumentState:", { store in { `self`, newValue in
        lock.sync {
            store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue)
        }} as @convention(block) (AnyObject, AnyObject) -> Void})
}

FAQ

Why didn't you call it Interpose? "Kit" feels so old-school.

Naming it Interpose was the plan, but then SR-898 came. While having a class with the same name as the module works in most cases, this breaks when you enable build-for-distribution. There's some discussion to get that fixed, but this will be more towards end of 2020, if even.

I want to hook into Swift! You made another ObjC swizzle thingy, why?

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 aka @_dynamicReplacement for details.)

Can I ship this?

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 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".

It does not do X!

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.

Installation

Building InterposeKit requires Xcode 11.4+ or a Swift 5.2+ toolchain with the Swift Package Manager.

Swift Package Manager

Add .package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1") to your Package.swift file's dependencies.

CocoaPods

InterposeKit is on CocoaPods. Add pod 'InterposeKit' to your Podfile.

Carthage

Add github "steipete/InterposeKit" to your Cartfile.

Improvement Ideas

  • Write proposal to allow to convert the calling convention of existing types.
  • Use the C block struct to perform type checking between Method type and C type (I do that in Aspects library), it's still a runtime crash but could be at hook time, not when we call it.
  • Add a way to get all current hooks from an object/class.
  • Add a way to revert hooks without super helper.
  • Add a way to apply multiple hooks to classes
  • Enable hooking of class methods.
  • Add dyld_dynamic_interpose to hook pure C functions
  • Combine Promise-API for Interpose.whenAvailable for better error bubbling.
  • Experiment with Swift function hooking? ⚡️
  • Test against Swift Nightly as Cron Job
  • 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.
  • I'm sure there's more - Pull Requests or comments very welcome!

Make this happen: Carthage compatible CocoaPods

Thanks

Special thanks to JP Simard who did such a great job in setting up Yams with GitHub Actions - this was extremely helpful to build CI here fast.

License

InterposeKit is MIT Licensed.

More Repositories

1

Aspects

Delightful, simple library for aspect oriented programming in Objective-C and Swift.
Objective-C
8,383
star
2

PSTCollectionView

Open Source, 100% API compatible replacement of UICollectionView for iOS4.3+
Objective-C
2,546
star
3

PSStackedView

open source implementation of Twitter/iPad stacked ui - done right.
Objective-C
1,970
star
4

AFDownloadRequestOperation

A progressive download operation for AFNetworking.
Objective-C
1,051
star
5

PSPDFTextView

A subclass of UITextView that fixes the most glaring problems from iOS 7 and 7.1.
Objective-C
877
star
6

PSTAlertController

API similar to UIAlertController, backwards compatible to iOS 7. Will use the new shiny API when you run iOS 8.
Objective-C
737
star
7

PSPushPopPressView

Zoom, Rotate, Drag – everything at the same time. A view-container for direct manipulation, inspired by Our Choice from Push Pop Press.
Objective-C
611
star
8

PSFoundation

Categories and helper classes for iOS projects.
Objective-C
570
star
9

PSTDelegateProxy

A simple proxy that forwards optional methods to delegates - less boilerplate in your code!
Objective-C
256
star
10

PSYouTubeExtractor

Display YouTube URLs in a MPMoviePlayerController
Objective-C
191
star
11

PSMenuItem

A block based UIMenuItem subclass.
Objective-C
179
star
12

PSPDFKit-Demo

A drop-in-ready framework that helps in almost every aspect of PDF-rendering on iOS.
Objective-C
178
star
13

UIKitDebugging

A set of files that enables various debug flags in UIKit
Objective-C
178
star
14

PSiOSAppTemplate

iOS Application Template with JSON-Parsing, AutoUpdating, CrashReporter+Sender, Statistics, custom Logging, Localization and all those little things already set up, ready for you to make awesome stuff!
Objective-C
174
star
15

PSAlertView

Modern block-based wrappers for UIAlertView and UIActionSheet.
Objective-C
129
star
16

PSStoreButton

UIButton that is styled like iPhone's AppStore-Button. No Images used!
Objective-C
91
star
17

PSTCenteredScrollView

Shows off different ways to center content in a UIScrollView.
Objective-C
79
star
18

NSLogger-CocoaLumberjack-connector

65
star
19

xcode-theme-solarized-modded

Heavily modded theme based on the Solarized style: http://ethanschoonover.com/solarized/
49
star
20

iOS6-Runtime-Headers

6.0 (GM)
Objective-C
47
star
21

speaking

Upcoming and past speaking engagements for Peter Steinberger (@steipete).
47
star
22

OSLogTest

Test app for OSLog
Swift
45
star
23

PSBackgroundCurtain

Fades the App to black/semi-black when in Background. Fully animated.
44
star
24

PSTFoundationBenchmark

Foundation Collection Classes Benchmarks
Objective-C
37
star
25

SwiftUITouchHandling

Testing touch handling from embedded SwiftUI views
Swift
28
star
26

stackoverflowerizer

always redirect to stackoverflow from pages that just copy the content, like efreedom
JavaScript
15
star
27

steipete.com

Personal Website of Peter Steinberger.
SCSS
13
star
28

dotfiles

my dot files for bash, git, rails and co
Shell
4
star