• Stars
    star
    8,383
  • Rank 4,381 (Top 0.09 %)
  • Language
    Objective-C
  • License
    MIT License
  • Created over 10 years ago
  • Updated about 4 years ago

Reviews

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

Repository Details

Delightful, simple library for aspect oriented programming in Objective-C and Swift.

Aspects v1.4.2 Build Status Carthage compatible

A delightful, simple library for aspect oriented programming by @steipete.

Think of Aspects as method swizzling on steroids. It allows you to add code to existing methods per class or per instance, whilst thinking of the insertion point e.g. before/instead/after. Aspects automatically deals with calling super and is easier to use than regular method swizzling.

Aspects hooks deep into the class hierarchy and creates dynamic subclasses, much like KVO. There's known issues with this approach, and to this date (February 2019) I STRICTLY DO NOT RECOMMEND TO USE Aspects IN PRODUCTION CODE. We use it for partial test mocks in, PSPDFKit, an iOS PDF framework that ships with apps like Dropbox or Evernote, it's also very useful for quickly hacking something up.

Aspects uses _objc_msgForward which causes issues with other code that uses message forwarding.

Aspects extends NSObject with the following methods:

/// Adds a block of code before/instead/after the current `selector` for a specific class.
///
/// @param block Aspects replicates the type signature of the method being hooked.
/// The first parameter will be `id<AspectInfo>`, followed by all parameters of the method.
/// These parameters are optional and will be filled to match the block signature.
/// You can even use an empty block, or one that simple gets `id<AspectInfo>`.
///
/// @note Hooking static methods is not supported.
/// @return A token which allows to later deregister the aspect.
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Adds a block of code before/instead/after the current `selector` for a specific instance.
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

/// Deregister an aspect.
/// @return YES if deregistration is successful, otherwise NO.
id<AspectToken> aspect = ...;
[aspect remove];

Adding aspects returns an opaque token of type AspectToken which can be used to deregister again. All calls are thread-safe.

Aspects uses Objective-C message forwarding to hook into messages. This will create some overhead. Don't add aspects to methods that are called a lot. Aspects is meant for view/controller code that is not called 1000 times per second.

Aspects calls and matches block arguments. Blocks without arguments are supported as well. The first block argument will be of type id<AspectInfo>.

When to use Aspects

Aspect-oriented programming (AOP) is used to encapsulate "cross-cutting" concerns. These are the kind of requirements that cut-across many modules in your system, and so cannot be encapsulated using normal object oriented programming. Some examples of these kinds of requirements:

  • Whenever a user invokes a method on the service client, security should be checked.
  • Whenever a user interacts with the store, a genius suggestion should be presented, based on their interaction.
  • All calls should be logged.

If we implemented the above requirements using regular OOP there'd be some drawbacks:

Good OOP says a class should have a single responsibility, however adding on extra cross-cutting requirements means a class that is taking on other responsibilites. For example you might have a StoreClient that is supposed to be all about making purchases from an online store. Add in some cross-cutting requirements and it might also have to take on the roles of logging, security and recommendations. This is not great because:

  • Our StoreClient is now harder to understand and maintain.
  • These cross-cutting requirements are duplicated and spread throughout our app.

AOP lets us modularize these cross-cutting requirements, and then cleanly identify all of the places they should be applied. As shown in the examples above cross-cutting requirements can be either technical or business focused in nature.

Here are some concrete examples:

Aspects can be used to dynamically add logging for debug builds only:

[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) {
    NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];

It can be used to greatly simplify your analytics setup: https://github.com/orta/ARAnalytics


You can check if methods are really being called in your test cases:

- (void)testExample {
    TestClass *testClass = [TestClass new];
    TestClass *testClass2 = [TestClass new];

    __block BOOL testCallCalled = NO;
    [testClass aspect_hookSelector:@selector(testCall) withOptions:AspectPositionAfter usingBlock:^{
        testCallCalled = YES;
    } error:NULL];

    [testClass2 testCallAndExecuteBlock:^{
        [testClass testCall];
    } error:NULL];
    XCTAssertTrue(testCallCalled, @"Calling testCallAndExecuteBlock must call testCall");
}

It can be really useful for debugging. Here I was curious when exactly the tap gesture changed state:

[_singleTapGesture aspect_hookSelector:@selector(setState:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
    NSLog(@"%@: %@", aspectInfo.instance, aspectInfo.arguments);
} error:NULL];

Another convenient use case is adding handlers for classes that you don't own. I've written it for use in PSPDFKit, where we require notifications when a view controller is being dismissed modally. This includes UIKit view controllers like MFMailComposeViewController and UIImagePickerController. We could have created subclasses for each of these controllers, but this would be quite a lot of unnecessary code. Aspects gives you a simpler solution for this problem:

@implementation UIViewController (DismissActionHook)

// Will add a dismiss action once the controller gets dismissed.
- (void)pspdf_addWillDismissAction:(void (^)(void))action {
    PSPDFAssert(action != NULL);

    [self aspect_hookSelector:@selector(viewWillDisappear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo) {
        if ([aspectInfo.instance isBeingDismissed]) {
            action();
        }
    } error:NULL];
}

@end

Debugging

Aspects identifies itself nicely in the stack trace, so it's easy to see if a method has been hooked:

Using Aspects with non-void return types

You can use the invocation object to customize the return value:

    [PSPDFDrawView aspect_hookSelector:@selector(shouldProcessTouches:withEvent:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> info, NSSet *touches, UIEvent *event) {
        // Call original implementation.
        BOOL processTouches;
        NSInvocation *invocation = info.originalInvocation;
        [invocation invoke];
        [invocation getReturnValue:&processTouches];

        if (processTouches) {
            processTouches = pspdf_stylusShouldProcessTouches(touches, event);
            [invocation setReturnValue:&processTouches];
        }
    } error:NULL];

Installation

The simplest option is to use pod "Aspects".

You can also add the two files Aspects.h/m to your project. There are no further requirements.

Compatibility and Limitations

Aspects uses quite some runtime trickery to achieve what it does. You can mostly mix this with regular method swizzling.

An important limitation is that for class-based hooking, a method can only be hooked once within the subclass hierarchy. See #2 This does not apply for objects that are hooked. Aspects creates a dynamic subclass here and has full control.

KVO works if observers are created after your calls aspect_hookSelector: It most likely will crash the other way around. Still looking for workarounds here - any help appreciated.

Because of ugly implementation details on the ObjC runtime, methods that return unions that also contain structs might not work correctly unless this code runs on the arm64 runtime.

Credits

The idea to use _objc_msgForward and parts of the NSInvocation argument selection is from the excellent ReactiveCocoa from the GitHub guys. This article explains how it works under the hood.

Supported iOS & SDK Versions

  • Aspects requires ARC.
  • Aspects is tested with iOS 7+ and OS X 10.7 or higher.

License

MIT licensed, Copyright (c) 2014 Peter Steinberger, [email protected], @steipete

Release Notes

Version 1.4.2

  • Allow to hook different subclasses.
  • Smaller tweaks.

Version 1.4.1

  • Rename error codes.

Version 1.4.0

  • Add support for block signatures that match method signatures. (thanks to @nickynick)

Version 1.3.1

  • Add support for OS X 10.7 or higher. (thanks to @ashfurrow)

Version 1.3.0

  • Add automatic deregistration.
  • Checks if the selector exists before trying to hook.
  • Improved dealloc hooking. (no more unsafe_unretained needed)
  • Better examples.
  • Always log errors.

Version 1.2.0

  • Adds error parameter.
  • Improvements in subclassing registration tracking.

Version 1.1.0

  • Renamed the files from NSObject+Aspects.m/h to just Aspects.m/h.
  • Removing now works via calling remove on the aspect token.
  • Allow hooking dealloc.
  • Fixes infinite loop if the same method is hooked for multiple classes. Hooking will only work for one class in the hierarchy.
  • Additional checks to prevent things like hooking retain/release/autorelease or forwardInvocation:
  • The original implementation of forwardInvocation is now correctly preserved.
  • Classes are properly cleaned up and restored to the original state after the last hook is deregistered.
  • Lots and lots of new test cases!

Version 1.0.1

  • Minor tweaks and documentation improvements.

Version 1.0.0

  • Initial release

More Repositories

1

PSTCollectionView

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

PSStackedView

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

AFDownloadRequestOperation

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

InterposeKit

A modern library to swizzle elegantly in Swift.
Swift
962
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
569
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

UIKitDebugging

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

PSPDFKit-Demo

A drop-in-ready framework that helps in almost every aspect of PDF-rendering on iOS.
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