TestsTested | ✗ |
LangLanguage | Obj-CObjective C |
License | MIT |
ReleasedLast Release | Dec 2014 |
Maintained by Unclaimed.
REKit [rikít] is a collection of extensions of NSObject
. Currently it provides 2 features:
Blocks and GCD (Grand Central Dispatch), caused a great change in the iOS, OS X world. It provided ability to write code which is executed in the future. Programmers' world became more flexible.
REKit (especially REResponder) brings out Blocks latent ability in a different way with GCD. REResponder provides ability to reconstruct instances. To be concrete, it provides ability to add/override methods at instance level. REKit can also cause great change.
REKit was used in iPhone app : SpliTron. REKit improved the development efficiency and maintainability. Thereby, SpliTron overcame some specification changes. REKit made the project team to focus on user experience.
REKit hopes to contribute to the iOS, OS X world.
REResponder provides ability to add/override methods at instance level. Details of features, behaviors, and efficient examples are below.
You can add methods to arbitrary instance dynamically using Block. To do so, you use -respondsToSelector:withKey:usingBlock:
method. For example, NSObject
doesn't have -sayHello
method, but you can add it as below:
id obj;
obj = [[NSObject alloc] init];
[obj respondsToSelector:@selector(sayHello) withKey:nil usingBlock:^(id receiver) {
NSLog(@"Hello World!");
}];
[obj performSelector:@selector(sayHello)]; // Hello World!
It is applied to obj
only, and it doesn't affect to the other instances.
You can override methods of arbitrary instance dynamically using Block. Same as "Adding methods dynamically", you use -respondsToSelector:withKey:usingBlock:
method. For example, if you have MyObject class logs "No!" in -sayHello
method, you can override it to log "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!
It is applied to obj
only, and it doesn't affect to the other instances.
As you can see above, receiver
argument is necessary for Block. The receier
argument is receiver of -respondsToSelector:withKey:usingBlock:
method. It doesn't cause retain cycle even if you use receiver
in the Block, so go ahead:
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 supports Block with arguments and/or return value. When you want to add or override method with arguments, list the arguments following receiver
argument:
UIAlertView *alertView;
// …
[alertView
respondsToSelector:@selector(alertViewShouldEnableFirstOtherButton:)
withKey:nil
usingBlock:^(id receiver, UIAlertView *alertView) {
return NO;
}
];
You can assign a key to Block, then you can manage Block with the key. To assign a key, you pass arbitrary object as key argument of -respondsToSelector:withKey:usingBlock:
method. You can check if an instance has a block by calling -hasBlockForSelector:withKey:
method. You can remove a block by calling -removeBlockForSelector:withKey:
method.
When an instance will be released, blocks added to the instance are removed automatically. If you don't need to manage a block specially, you can pass nil as the key argument - UUID string will be assigned to the block internally.
An instance stacks Blocks per selector. The last added (upper most) Block is the block executed when the selector is called. When you try to add a Block with already existing key, old one will be removed, then new one will be stacked at the top.
You can invoke supermethod - implementation of Block under the current Block, or hard-coded implementation - using -supermethodOfCurrentBlock
method:
[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;
}];
The supermethod needs receiver and selector arguments at least. If the selector has arguments, list them following selector argument.
Cast supermethod if it's needed. For example, below is a cast of supermethod which returns CGRect:
typedef CGRect (*RectIMP)(id, SEL, ...);
RectIMP supermethod;
if ((supermethod = (RectIMP)[receiver supermethodOfCurrentBlock])) {
rect = supermethod(receiver, @selector(rect));
}
The following sections illustrate some efficient examples of REResponder.
A class using delegation pattern keeps reusability by excluding application contexts and provides delegate methods to plug application contexts into it. If you can plug application contexts into an instance, you can make an instance independent from delegate object which is on application layer. REResponder makes it possible.
The code fragments below illustrate how you can set alertView
itself as the delegate to 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;
The code fragments below illustrate how you can set animation
itself as the delegate to animation
:
CABasicAnimation *animation;
// …
[animation
respondsToSelector:@selector(animationDidStop:finished:)
withKey:nil
usingBlock:^(id receiver, CABasicAnimation *animation, BOOL finished) {
// Do something…
}
];
animation.delegate = animation;
Pros:
For target/action paradigm, you can use similar technique described in "Delegate itself". The code fragments below adds button - whose target is button itself - to 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 is also useful for UnitTest. The code fragments below check whether delegate method of BalloonController
is called, using mock object:
__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, @"");
The code fragments below stub out download process of AccountManager, then test view controller of 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 helps you to gather related code fragments into one place. For example, if you want to add code fragments about UIKeyboardWillShowNotification
, you can gather them into -_manageKeyboardWillShowNotificationObserver
method like below:
- (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;
}];
}
a. Class is changed
When you add/override methods, the instance becomes an instance of class named "REResponder_UUID_OriginalClassName". It turned out that it breaks relationships of KVO. The problem has been fixed already, but some other problems can happen. If the problems happen, cope with them using -willChangeClass:
and -didChangeClass:
, or REObjectWillChangeClassNotification
and REObjectDidChangeClassNotification
.
REObserver provides:
REObserver provides -addObserverForKeyPath:options:usingBlock:
method. You can pass a block to be executed when the value is changed:
id observer;
observer = [obj addObserverForKeyPath:@"someKeyPath" options:0 usingBlock:^(NSDictionary *change) {
// Do something…
}];
Pros:
You can stop observing with -stopObserving
method:
[observer stopObserving];
With code above, observer
stops all observations. There is no need to remember observed object, keyPath, and context.
When observed object or observing object is released, REObserver stops related observations automatically. It solves problems below (Below is Non-ARC code):
- (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 and later
OS X 10.7 and later
You can install REKit using CocoaPods.
<Podfile for iOS>
platform :ios, '5.0'
pod 'REKit'
<Podfile for OS X>
platform :osx, '10.7'
pod 'REKit'
<Terminal>
$ pod install
If you want to install REKit manually, add files which is under REKit folder to your project.
MIT License. For more detail, read LICENSE file.