AspectsPlus

Delightful, simple library for aspect oriented programming.


License
MIT
Install
pod try AspectsPlus

Documentation

AspectsPlus   Build Status codecov Carthage compatible

AspectsPlus is a new implementation based on Aspects.

I add the AspectsConfig to Aspects.

@interface AspectsConfig : NSObject

+ (instancetype)sharedAspectsConfig;

@property(nonatomic,strong)NSSet *customClassBlackList;
@property(nonatomic,strong)NSDictionary *customClassMethodBlackList;

/// key :class string NSString  value:method string NSSet
///
/// @{@"UIViewController":[NSSet setWithObjects:@"viewDidAppear:", nil]}
///
@property(nonatomic,strong)NSDictionary *onceHookClassMethodMap;

@property(nonatomic,strong)NSDictionary *onceHookWhiteListClassMethodMap;

/// Only run in instance method ,It's like Aspects default.  Default YES
@property(nonatomic,assign)BOOL instanceMethodOnceHook;

/// methodOnceHook > instanceMethodOnceHook
@property(nonatomic,assign)BOOL methodOnceHook;

/// if you want use unFindMethodToAdd,must be the block like ^(id instance,id argument1,id argument2,...){} or ^(id instance,...){}
@property(nonatomic,assign)BOOL unFindMethodToAdd;

@end

#define AspectsConfigInstance [AspectsConfig sharedAspectsConfig]

Usage

AspectsConfigInstance.unFindMethodToAdd=YES;
AspectsConfigInstance.methodOnceHook=NO;
AspectsConfigInstance.instanceMethodOnceHook=NO;

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

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