background
Many common controls in UIKit implement event callbacks through Delegate mode or specify target+selector, such as UITableView, UITextField, UIButton, etc. The advantage of this approach is that the code is neat and easier to maintain when there is a lot of code. However, when the callback logic is not particularly complex, using a Block callback has some advantages over Delegate or target+selector.
- Compact code, no need to declare the protocol, can be related to the code logic together, reduce the cost of development and debugging;
- Allows access to context variables without having to extract instance variables to be shared by different proxy methods.
Apple itself has tweaked some of its apis to support Block callbacks, such as NSTimer, which added methods after iOS 10:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats Block:(void (^) (NSTimer *timer))Block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
Copy the code
To replace the selector method previously specified:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
Copy the code
Another example is UIAlertViewController, which was used before iOS 9, and to implement callbacks via UIAlertViewDelegate, Apple scrapped a UIAlertController class, pulled out UIAlertAction, Complete Block implementation, code to write a lot of concise:
+ (instancetype)actionWithTitle:(nullable NSString *)title style:(UIAlertActionStyle)style handler:(void (^ __nullable)(UIAlertAction *action))handler;
Copy the code
Optimization idea
In view of the above analysis, the Block rewriting of UITableView, UITextField, UIButton and other commonly used UIKit classes is expected to do the following:
- in
Delegate
On the basis of the corresponding increaseBlock
Manner, originalDelegate
The callback mode is not affected. The caller can select an appropriate callback mode based on the actual scenario. Block
The method and the originalDelegate
Method names should be kept consistent to reduce migration costs.- The assignment
Block
When the callback,Xcode
To be able to automatically code fill, because handwritingBlock
Input and return parameters are prone to error; - Use as little as possible
method swizzling
And other dark magic to minimize the impact on security and stability.
HWEasyUI
Based on the above purpose, the author encapsulated HWEasyUI library, to UITableView, UITextField, UIButton commonly used UI components to do Block transformation, use the following example:
UITableView implements a simple list:
UITableView *tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:reuseId];
[self.view addSubview:tableView];
NSArray *titles= @ [@ "Beijing," @ "Shanghai," @ "shenzhen" @ "in guangzhou," @ "in chengdu," @ "male" Ann. @ "suzhou");tableView.numberOfRowsHandler = ^NSInteger(UITableView *__weak _Nonnull tableView.NSInteger section) {
return titles.count;
};
tableView.cellForRowHandler = ^UITableViewCell * _Nonnull(UITableView *__weak _Nonnull tableView, NSIndexPath * _Nonnull indexPath) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseId forIndexPath:indexPath];
cell.textLabel.text = titles[indexPath.row];
return cell;
};
tableView.didSelectRowHandler = ^(UITableView *__weak _Nonnull tableView, NSIndexPath * _Nonnull indexPath) {
NSString *title = titles[indexPath.row];
NSLog(title);
};
Copy the code
UITextField implements an input field that allows up to 6 characters:
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(20.100.self.view.frame.size.width - 40.30)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.clearButtonMode = UITextFieldViewModeAlways;
[self.view addSubview:textField];
textField.shouldChangeCharactersHandler = ^BOOL(UITextField *__weak _Nonnull textField, NSRange range, NSString * _Nonnull replacementString) {
NSString *str = [textField.text stringByReplacingCharactersInRange:range withString:replacementString];
if (str.length > 6) {
return NO;
}
return YES;
};
textField.shouldReturnHandler = ^BOOL(UITextField *__weak _Nonnull textField) {
[textField resignFirstResponder];
return YES;
};
Copy the code
UIButton, considering most of UIControlEventsTouchUpInside incident response, so the special seal for a clickHandler, response can be used for other events setEventsHandler: forControlEvents: :
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setFrame:CGRectMake(24.200.self.view.frame.size.width - 48.20)];
[btn setTitle:@"OK" forState:UIControlStateNormal];
btn.clickHandler = ^{
NSLog(@"OK");
};
[self.view addSubview:btn];
Copy the code
Realize the principle of
The core of UIKit Block modification is:
- To be reformed
UIKit
Class, add the correspondingBlock
Properties; - Because it can’t be modified
UIKit
Source code, so you still need to have oneDelegate
Object to implement the corresponding proxy method; Delegate
Object finds the corresponding when executing the proxy methodBlock
Execute the actual callback method;- Hide this from the caller
Delegate
Object;
The following takes UITextField as an example to look at the main process of transformation:
Add a Block attribute
Add the corresponding category and bind the Block to runtime. Note that the name and parameters of the Block must be consistent with the Delegate method. Define Block attributes in header files:
typedef BOOL(^HWShouldBeginEditingBlock) (UITextField *__weak textField);
@property (nonatomic, copy) HWShouldBeginEditingBlock shouldBeginEditingHandler;
Copy the code
In the implementation file, implement the corresponding setter and getter:
- (void)setShouldBeginEditingHandler:(HWShouldBeginEditingBlock)shouldBeginEditingHandler {
NSAssert(shouldBeginEditingHandler, @"shouldBeginEditingHandler cannot be nil");
[self configDelegate];
objc_setAssociatedObject(self.HWEasyUIShouldBeginEditingKey, shouldBeginEditingHandler, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (HWShouldBeginEditingBlock)shouldBeginEditingHandler {
return objc_getAssociatedObject(self.&HWEasyUIShouldBeginEditingKey);
}
Copy the code
Here, the setter will execute [self configDelegate] at the same time, and we’ll talk about the purpose.
Configuration Delegte
A new class, HWEasyUIProxy, follows UITextFieldDelegate, whose delegate method actually executes the Block bound to the object, and returns the default if no corresponding Block is found:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (textField.shouldBeginEditingHandler) {
return textField.shouldBeginEditingHandler(textField);
}
return YES;
}
Copy the code
When you set the Block property in the previous step, you set the Delegate to the HWEasyUIProxy:
- (void)configDelegate {
HWEasyUIProxy *Delegate = [HWEasyUIProxy sharedInstance];
if (self.Delegate ! = Delegate) {
self.Delegate = Delegate; }}Copy the code
Hide the Delegate from the caller
Because every time a Block is set, a check is made to check the set Delegate, it is possible to hide the Delegate from the caller. Considering the usage characteristics and frequency of HWEasyUIProxy, and since it does not contain instance variables and is only used for forwarding methods, it is convenient to use the singleton form.
Memory processing
typedef BOOL(^HWShouldChangeCharactersBlock) (UITextField *__weak textField, NSRange range, NSString *replacementString);
Copy the code
When defining blocks, UIKit objects themselves need to be set to an __weak property to prevent loops between UIKit objects and their holding blocks.
conclusion
The implementation of HWEasyUI is mostly glue code, but it’s all worth it if it makes it easier for callers to use and less expensive to maintain. Welcome to discuss, use and improve.