REKit 1.0.2

REKit 1.0.2

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布上次发布2014年12月

未指定 维护。



REKit 1.0.2

  • 作者
  • Kazki Miura

REKit [rikít] 是 NSObject 扩展集合。目前它提供 2 个功能

  1. REResponder:提供在实例级别添加/覆盖方法的能力
  2. REObserver:提供与 Blocks 兼容的 KVO(键值编码)方法 + α

Blocks 和 GCD(Grand Central Dispatch)在 iOS、OS X 世界中带来了巨大变化。它提供了编写未来执行代码的能力。程序员的领域变得更加灵活。

REKit(特别是 REResponder)以与 GCD 不同方式带来了 Blocks 的潜在能力。REResponder 提供了重建实例的能力。具体来说,它提供了在实例级别添加/覆盖方法的能力。REKit 也可以带来巨大变化。

REKit 在 iPhone 应用程序 SpliTron 中使用。REKit 提高了开发效率和可维护性。因此,SpliTron 走过了某些规范变化。REKit 使项目团队能够专注于用户体验。

REKit 希望为 iOS、OS X 世界做出贡献。

REResponder

REResponder 提供在实例级别添加/覆盖方法的能力。以下是功能的详情、行为和 高效示例

动态添加方法

您可以使用 Block 在任意实例上动态添加方法。为此,您使用 -respondsToSelector:withKey:usingBlock: 方法。例如,NSObject 没有具有 -sayHello 方法的,但您可以添加如下

id obj;
obj = [[NSObject alloc] init];
[obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) {
    NSLog(@"Hello World!");
}];
[obj performSelector:@selector(sayHello)]; // Hello World!

它只应用于 obj,不会影响其他实例。

动态覆盖方法

您可以使用 Block 在任意实例上动态覆盖方法。与 "动态添加方法" 相似,您使用 -respondsToSelector:withKey:usingBlock: 方法。例如,如果您有一个类 MyObject 在 -sayHello 方法中记录 "No!",您可以将它覆盖为记录 "Hello World!"

MyObject *obj;
obj = [[MyObject alloc] init];
// [obj sayHello]; // No!   
[obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) {
    NSLog(@"Hello World!");
}];
[obj sayHello]; // Hello World!

它只应用于 obj,不会影响其他实例。

Block 的接收器参数

如上所示,receiver 参数是必需的。该参数是 -respondsToSelector:withKey:usingBlock: 方法的接收器。即使您在 Block 中使用 receiver,它也不会导致保持周期,所以请继续。

id obj;
obj = [[NSObject alloc] init];
[obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) {
    // NSLog(@"obj = %@", obj); // Causes retain cycle! Use receiver instead.
    NSLog(@"receiver = %@", receiver);
}];
[obj performSelector:@selector(sayHello)];

带参数和/或返回值的块

REResponder 支持带参数和/或返回值的块。当你想要添加或重写带参数的方法时,在receiver参数之后列出参数

UIAlertView *alertView;
// …
[alertView
    respondsToSelector:@selector(alertViewShouldEnableFirstOtherButton:)
    withKey:nil
    usingBlock:^(id receiver, UIAlertView *alertView) {
        return NO;
    }
];

使用键管理块

你可以给块分配一个键,然后你可以使用这个键来管理块。要分配键,你可以将任意对象作为方法-respondsToSelector:withKey:usingBlock:的键参数传递。你可以通过调用方法-hasBlockForSelector:withKey:来检查一个实例是否有一个块。你可以通过调用方法-removeBlockForSelector:withKey:来移除一个块。

当一个实例将要被释放时,添加到该实例的块将被自动移除。如果你不需要特别管理一个块,你可以将 nil 作为键参数传递 - 块内部将分配一个 UUID 字符串。

块栈

实例按选择器堆叠块。最后添加的(最顶部的)块是在调用选择器时执行的块。当你尝试添加具有现有键的块时,旧块将被移除,然后新块将被堆叠在顶部。

调用超方法

你可以使用方法-supermethodOfCurrentBlock调用超方法 - 当前块下的块实现,或者硬编码的实现。

[obj respondsToSelector:@selector(description) withKey:nil usingBlock:^(id receiver) {
    // Make description…
    NSMutableString *description;
    description = [NSMutableString string];

    // Append original description
    IMP supermethod;
    if ((supermethod = [receiver supermethodOfCurrentBlock])) {
        [description appendString:supermethod(receiver, @selector(description))];
    }

    // Customize description…

    return description;
}];

超方法需要至少接收者和选择器参数。如果选择器有参数,请在选择器参数之后列出它们。

如果需要,请转换超方法。例如,下面是返回 CGRect 的超方法的转换

typedef CGRect (*RectIMP)(id, SEL, ...);
RectIMP supermethod;
if ((supermethod = (RectIMP)[receiver supermethodOfCurrentBlock])) {
    rect = supermethod(receiver, @selector(rect));
}

高效示例

以下段落展示了 REResponder 的一些高效示例。

委派自身

使用代理模式的项目通过排除应用程序上下文来保持可重用性,并提供代理方法将应用程序上下文连接到它。如果你可以将应用程序上下文插入到一个实例中,你就可以使该实例不依赖于位于应用层上的委托对象。REResponder 就可以实现这一点。

下面的代码片段说明了如何将alertView本身作为代理设置给alertView

UIAlertView *alertView;
alertView = [[UIAlertView alloc]
    initWithTitle:@"title"
    message:@"message"
    delegate:nil
    cancelButtonTitle:@"Cancel"
    otherButtonTitles:@"OK", nil
];
[alertView
    respondsToSelector:@selector(alertView:didDismissWithButtonIndex:)
    withKey:nil
    usingBlock:^(id receiver, UIAlertView *alertView, NSInteger buttonIndex) {
        // Do something…
    }
];
alertView.delegate = alertView;

下面的代码片段说明了如何将animation本身作为代理设置给animation

CABasicAnimation *animation;
// …
[animation
    respondsToSelector:@selector(animationDidStop:finished:)
    withKey:nil
    usingBlock:^(id receiver, CABasicAnimation *animation, BOOL finished) {
        // Do something…
    }
];
animation.delegate = animation;

优点

  • 你可以将相关的代码片段集中在一个地方。这提高了可维护性。
  • 无需担心 "谁的代理方法被调用?"。
  • 无需担心 "代理是僵尸?"。

目标自身

对于目标/动作范例,你可以使用在 "委派自身" 部分中描述的类似技术。下面的代码片段向 UICollectionViewCell 添加了按钮 - 其目标为按钮本身。

UIButton *button;
// …
[button respondsToSelector:@selector(buttonAction) withKey:@"key" usingBlock:^(id receiver) {
    // Do something…
}];
[button addTarget:button action:@selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:button];

单元测试中的模拟对象

REResponder 也可用于单元测试。下面的代码片段使用模拟对象检查 BalloonController 的代理方法是否被调用。

__block BOOL called = NO;

// Make mock
id mock;
mock = [[NSObject alloc] init];
[mock
    respondsToSelector:@selector(balloonControllerDidDismissBalloon:)
    withKey:nil
    usingBlock:^(id receiver, BalloonController *balloonController) {
        called = YES;
    }
];
balloonController.delegate = mock;

// Dismiss balloon
[balloonController dismissBalloonAnimated:NO];
STAssertTrue(called, @"");

单元测试中的高成本过程模拟

下面的代码片段模拟了 AccountManager 的下载过程,然后测试 account-view 的视图控制器。

// Load sample image
__weak UIImage *sampleImage;
NSString *sampleImagePath;
sampleImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"sample" ofType:@"png"];
sampleImage = [UIImage imageWithContentsOfFile:sampleImagePath];

// Stub out download process
[[AccountManager sharedManager]
    respondsToSelector:@selector(downloadProfileImageWithCompletion:)
    withKey:@"key"
    usingBlock:^(id receiver, void (^completion)(UIImage*, NSError*)) {
        // Execute completion block with sampleImage
        completion(sampleImage, nil);

        // Remove current block
        [receiver removeCurrentBlock];
    }
];

// Call thumbnailButtonAction which causes download of profile image
[acccountViewController thumbnailButtonAction];
STAssertEqualObjects(accountViewController.profileImageView.image, sampleImage, @"");

收集代码片段

REResponder帮助您将相关的代码片段收集到一个地方。例如,如果您想添加关于UIKeyboardWillShowNotification的代码片段,您可以将其收集到以下-_manageKeyboardWillShowNotificationObserver方法中

- (id)initWithCoder:(NSCoder *)aDecoder
{
    // super
    self = [super initWithCoder:aDecoder];
    if (!self) {
        return nil;
    }

    // Manage _keyboardWillShowNotificationObserver
    [self _manageKeyboardWillShowNotificationObserver];

    return self;
}

- (void)_manageKeyboardWillShowNotificationObserver
{
    __block id observer;
    observer = _keyboardWillShowNotificationObserver;

    #pragma mark └ [self viewWillAppear:]
    [self respondsToSelector:@selector(viewWillAppear:) withKey:nil usingBlock:^(id receiver, BOOL animated) {
        // supermethod
        REVoidIMP supermethod; // REVoidIMP is defined like this: typedef void (*REVoidIMP)(id, SEL, ...);
        if ((supermethod = (REVoidIMP)[receiver supermethodOfCurrentBlock])) {
            supermethod(receiver, @selector(viewWillAppear:), animated);
        }

        // Start observing
        if (!observer) {
            observer = [[NSNotificationCenter defaultCenter]
                addObserverForName:UIKeyboardWillShowNotification
                object:nil
                queue:[NSOperationQueue mainQueue]
                usingBlock:^(NSNotification *note) {
                    // Do something…
                }
            ];
        }
    }];

    #pragma mark └ [self viewDidDisappear:]
    [self respondsToSelector:@selector(viewDidDisappear:) withKey:nil usingBlock:^(id receiver, BOOL animated) {
        // supermethod
        REVoidIMP supermethod;
        if ((supermethod = (REVoidIMP)[receiver supermethodOfCurrentBlock])) {
            supermethod(receiver, @selector(viewDidDisappear:), animated);
        }

        // Stop observing
        [[NSNotificationCenter defaultCenter] removeObserver:observer];
        observer = nil;
    }];
}

REResponder已知问题

a. 类已更改
当您添加/覆盖方法时,实例变为名为"REResponder_UUID_OriginalClassName"的类的实例。结果是它打破了KVO的关系。问题已经得到修复,但仍可能发生其他问题。如果出现问题,请使用-willChangeClass:-didChangeClass:REObjectWillChangeClassNotificationREObjectDidChangeClassNotification来处理它们。

REObserver

REObserver提供了

  1. 与KVO兼容的块方法
  2. 简单的停止观察方法
  3. 自动停止观察系统

与KVO兼容的块方法

REObserver提供了-addObserverForKeyPath:options:usingBlock:方法。您可以将一个块传递给它,当值更改时执行该块

id observer;
observer = [obj addObserverForKeyPath:@"someKeyPath" options:0 usingBlock:^(NSDictionary *change) {
    // Do something…
}];

优点

  • 你可以将相关的代码片段集中在一个地方。这提高了可维护性。
  • 您无需担心“哪个键的哪个值改变了?”。
  • 您无需创建上下文对象,因为块可以保持上下文。

简单停止观察方法

您可以使用-stopObserving方法停止观察

[observer stopObserving];

如上述代码所示,observer停止了所有的观察。无需记住观察对象、键路径和上下文。

自动停止观察系统

当观察对象或观察对象被释放时,REObserver会自动停止相关的观察。它解决了以下问题(以下是非ARC代码)

- (void)problem1
{
    UIView *view;
    view = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
    @autoreleasepool {
        id observer;
        observer = [[[NSObject alloc] init] autorelease];
        [view addObserver:observer forKeyPath:@"backgroundColor" options:0 context:nil];
    }
    NSLog(@"observationInfo = %@", (id)[view observationInfo]); // view is observed by zombie!
    view.backgroundColor = [UIColor redColor]; // Crash!
}

- (void)problem2
{
    id observer;
    observer = [[[NSObject alloc] init] autorelease];
    @autoreleasepool {
        UIView *view;
        view = [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
        [view addObserver:observer forKeyPath:@"backgroundColor" options:0 context:nil];
    }
    // observer is observing zombie!
}

可用性

iOS 5.0及以上

OS X 10.7及以上

安装

您可以使用CocoaPods安装REKit。

<Podfile for iOS>

platform :ios, '5.0'
pod 'REKit'

<Podfile for OS X>

platform :osx, '10.7'
pod 'REKit'

<Terminal>

$ pod install

如果您想手动安装REKit,将REKit文件夹下的文件添加到您的项目中。

许可证

MIT许可证。有关更多详情,请参阅LICENSE文件。