换个思路,让编程更加有意思

记录一些自己平常使用的另类编程方法,给繁复枯燥的代码平添一些乐趣。主要是从一些实际使用场景出发,探讨如何『偷懒』,让时间不那么浪费在重复修改代码上,即使修改也要让修改代码没那么麻烦。

目的:

  • 解决冗余代码
  • 减少样板代码的书写
  • 使用方便

1.关于ActionSheet点击响应不同事件的一个使用场景

比较常用的一个场景是一个界面中,我们需要弹出来一个ActionSheet,里面有n多个选项可供用户来选择。从产品设计的角度来说,对应的n种操作就是一个可怕的定时炸弹,不确定产品是不是会对这n种选项进行一个任性的排列;而从写代码的角度来说,对于这n种操作,写一个if-else或者switch-case是一个正确的选择,但是写下来你就会发现:冗余。再加上有可能会有排序的改动,那更加是毁灭性的代码。

对于这种类型的地方,没有必要花费太多精力来书写以及维护代码,需要的是简洁且易维护。

可以考虑使用数组将分支的操作进行存储,然后在点击选择时就可以直观的进行其对应的操作。可惜在 中,选择子@selector既不是基础数据类型也不是对象,是不能够使用数组存储的,那么就只能使用C数组来存储了,写出来大概是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {

    SEL selectors[] = {
        @selector(insertRow),
        @selector(insertSection),
        @selector(deleteSection)
    };

    if (buttonIndex < sizeof(selectors) / sizeof(SEL)) {

        void(*imp)(id, SEL) = (typeof(imp))[self methodForSelector:selectors[buttonIndex]];

        imp(self, selectors[buttonIndex]);
    }
}

这样还是会带来两个问题,第一个是UIActionSheet的实现和方法是分开写的,第二个是即使这样还是需要维护两个数组,没有达到易维护的目的,但是好赖也比以前简单了许多。

第一个问题可以使用最新的UIAlertController的block来解决,但是这无疑又是一个问题,要写好多样板代码,这时候就需要对UIAlertController进行一下封装,使其使用起来更加简洁。

2.对系统的UIAlertController进行封装

在iOS8.0之后系统就不推荐使用UIAlertView以及UIActionSheet,而使用了一个UIAlertController来替换他们两个。这样的做法对于混乱的视图展示层级来说确实是一个福音,并且可以使用block的方式来在一处处理对应的事件。但是,对于新的UIAlertController来说,要写好多的样板代码,一般来说,排版也不是很好看。

基于减少劳动力以及优化使用方式的目的,对系统的UIAlertController进行一层简单的封装。思路是使用协议的方式来约定是要显示的Alert以及ActionSheet类:

1
2
3
4
5
6
7
8
@protocol HLLAlertActionSheetProtocol <NSObject>
@optional
#pragma mark - config

- (id<HLLAlertActionSheetProtocol>) title:(NSString *)titile;
- (id<HLLAlertActionSheetProtocol>) message:(NSString *)message;
...
@end

使用一个HLLAlertActionSheet类来生成对应的alert或者actionSheet类:

1
2
3
4
5
6
7
8
9
10
11
@interface HLLAlertActionSheet : NSObject
+ (id<HLLAlertActionSheetProtocol>) alert;
+ (id<HLLAlertActionSheetProtocol>) actionSheet;
@end

@implementation HLLAlertActionSheet
#pragma mark - config
+ (id<HLLAlertActionSheetProtocol>) alert{
return [HLLAlert alert];
}
@end

而对于alert以及actionSheet类来说,内部使用一个私有类来存储通过实现协议传入的设置,然后在showIn:方法中对UIalertContrller进行初始化、设置以及显示。一个alert类的部分内容如下:

1
2
3
4
5
6
@interface HLLAlert : NSObject<HLLAlertActionSheetProtocol>

@property (nonatomic ,strong) HLLAlertActionSheetModel * aModel;
#pragma mark - config
+ (HLLAlert *) alert;
@end

其中实现协议方法时候把当前实例对象也返回出去:

1
2
3
4
5
- (HLLAlert *) title:(NSString *)titile{

self.aModel.title = titile;
return self;
}

这样在使用的时候可以这样:

1
2
3
4
5
6
7
8
9
[[[[[[[HLLAlertActionSheet alert]
title:@"举报这个人"]
message:@"确定举报当前主播直播的内容违规"]
buttons:@[@"取消",@"举报"]] style:UIAlertActionStyleCancel index:0]
showIn:self] fetchClick:^(NSInteger index) {

if (index = 1) {// 举报
}
}];

对于传入的用于可选操作的内容,可以使用数组也可以使用类似于UIAlertView初始化的时候不定个数的字符串类型:

1
2
3
4
/** Default handle button type is `UIAlertActionStyleDefault`. */
- (id<HLLAlertActionSheetProtocol>) buttons:(NSArray *)buttons;
/** Explain some as above .这个采用多字符串的方式传递buttons,以应对button-fetch不对应的问题*/
- (id<HLLAlertActionSheetProtocol>) buttonTitles:(NSString *)buttonTitles, ... NS_REQUIRES_NIL_TERMINATION;

目前,没有做可以添加UITextField的封装,后续有时间再做。

3.关于一个变量的实例化方式

其实这是小括号内联符合表达式写法–A compound statement enclosed in parentheses.

想象一下,在对一个类的对象变量进行实例化的时候,往往需要写大量的关于这个对象变量的设置属性代码。

这种做法的最大的意义是将代码整理分块,将同一个逻辑的代码抱在一起;同时对于一个不需要复用的小段逻辑,免去了重量级函数的调用:

1
2
3
4
5
self.loginButton = ({
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        ...
        button;
    });

4.使用位域进行代理方法实现与否的判断

在使用代理的时候需要判断代理代理是否实现了协议方法,一般的方法就是使用if-else进行书写,但是这样的切砖代码比较枯燥,也没有一点儿技术性可言,所以按照EOC中的方法可以使用以下方法进行书写,

使用结构体进行位的标记位:

1
2
3
4
5
6
7
@interface KKGestureLockView (){
    struct {
        unsigned int didBeginWithPasscode :1;
        unsigned int didEndWithPasscode : 1;
        unsigned int didCanceled : 1;
    } _delegateFlags;
}

在设置代理的时候进行判断代理是否实现了相关协议方法,然后设置标记位:

1
2
3
4
5
6
7
- (void)setDelegate:(id<KKGestureLockViewDelegate>)delegate{

    _delegate = delegate; 

    _delegateFlags.didBeginWithPasscode = [delegate respondsToSelector:@selector(gestureLockView:didBeginWithPasscode:)];
    _delegateFlags.didEndWithPasscode = 。。。
}

使用标记位判断是否实现协议方法:

1
2
3
4
5
if (_delegateFlags.didBeginWithPasscode) {

     [self.delegate gestureLockView:self didBeginWithPasscode:[NSString stringWithFormat:@"%zd",touchedButton.tag]];

}

5.改进属性字符串

在使用属性字符串的时候,不论生成还是对已知字符串进行某些字段设置属性,都是一件很麻烦的事情,要编写大段的样板代码实在是浪费时间。对生成配置属性字符串的操作进行简化,但是不要放弃系统提供的设置方法,因为系统的都会用,改进之后会好上手。

改进生成属性字符串的方式,简单需求比如一个个的拼接:

1
2
3
4
5
6
7
8
9
10
// black 16
+ (instancetype) builder;
// { NS...AttributeName : (id)value}
+ (instancetype) builderWithDefaultStyle:(NSDictionary *)defaultStyle;

- (HLLAttributedBuilder *) appendString:(NSString *)string;
- (HLLAttributedBuilder *(^)(NSString *str)) appendString;

- (HLLAttributedBuilder *) appendString:(NSString *)string forStyle:(NSDictionary *)style;
- (HLLAttributedBuilder *(^)(NSString *str ,NSDictionary *style)) appendStringAndStyle;

更常见的需求比如,查找特定的字符串然后进行属性设置。一种情形是,已有一个字符串,需要对其中的某一些字符串进行属性字符串设置,支持正则表达式匹配:

1
2
3
4
5
+ (instancetype) builderWithString:(NSString *)originalString;
+ (instancetype) builderWithString:(NSString *)originalString defaultStyle:(NSDictionary *)defaultStyle;

- (HLLAttributedBuilder *) configString:(NSString *)string forStyle:(NSDictionary *)style;
- (HLLAttributedBuilder *(^)(NSString *str ,NSDictionary *style)) configStringAndStyle;

根据上面设置的属性然后调用- (NSAttributedString *) attributedString;方法就可以生成对应的属性字符串。

使用例子

1
2
3
4
5
6
7
8
9
10
11
12
NSString * display = @"hello = nihao = Hello = 你好 = nihao";
attachment 是NSTextAttachment类的实例
[[[[[[[HLLAttributedBuilder builderWithString:display]
configString:@"hello" forStyle:@{NSUnderlineColorAttributeName:[UIColor redColor],
NSUnderlineStyleAttributeName:@1,
NSForegroundColorAttributeName:[UIColor orangeColor]}]
configString:@"nihao" forStyle:@{NSStrokeColorAttributeName:[UIColor redColor],
NSStrokeWidthAttributeName:@1}]
configString:@"H" forStyle:@{NSBackgroundColorAttributeName:[UIColor greenColor]}]
appendAttachment:attachment]
appendString:@"娃大喜"]
attributedString];

内部主要是使用私有类来记录每次的属性设置,用attributedString属性来记录,然后使用数组来保存这个私有实例。对于-append...方式,每次将记录的对象放入数组,而对于-configString...方式,则是数组中一直只有一个对象。

6.简化NSUserDefaults的使用

NSUserDefaults一般用来保存一些简单的数据到本地,但是写起来会写很多的样板代码,没有什么技术含量,稍微不注意还会出错,这里将NSUserDefaults添加一个分类,简化他的使用。

结合宏和block的特性,可以很优雅的使用点语法调用。

1
2
3
4
5
6
7
8
9
10
#define MM_UserDefaults [NSUserDefaults standardUserDefaults]

@interface NSUserDefaults (MM_Tools)

- (NSUserDefaults *(^)(NSString *key,BOOL value))mm_addBool;
- (BOOL(^)(NSString *))mm_boolValue;
...
- (NSUserDefaults *(^)(NSString *key,id value))mm_addObject;
- (id(^)(NSString *))mm_objectValue;
@end

对应的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (NSUserDefaults *(^)(NSString *,BOOL))mm_addBool{
return ^NSUserDefaults *(NSString *key,BOOL value){
return self.mm_addObject(key,@(value));
};
}
- (BOOL(^)(NSString *))mm_boolValue{

return ^BOOL(NSString *key){
return [self.mm_objectValue(key) boolValue];
};
}
...
- (NSUserDefaults *(^)(NSString *,id))mm_addObject{
return ^NSUserDefaults *(NSString *key,id value){
[self setValue:value forKey:key];
[self synchronize];
return self;
};
}
- (id(^)(NSString *))mm_objectValue{
return ^id(NSString *key){
return [self valueForKey:key];
};
}

使用的时候可以这样

1
2
MM_UserDefaults.mm_addBool(@"some_bool_key",YES);
MM_UserDefaults.mm_boolValue(@"some_bool_key");

7.优雅的为分类添加属性

难保会遇到要在某一个分类中添加好多的属性,结合runtime可以实现,但是要写好多的重复代码,对于这些重复代码,使用宏来统一替换是一个很好的方法。

借由下面文章中的方法,实现为一个单例添加一些属性,结合上面的MM_UserDefaults对这些属性进行本地存储

1
2
3
4
5
6
7
8
9
10
11
@interface MM_Manager (UserDefaults)

mm_property_basicDataType(BOOL, openDebugerToggle
...
@end

@implementation MM_Manager (UserDefaults)

mm_def_property_basicDataType(BOOL, openDebugerToggle)

@end

宏里面有两个对当前类的方法需要添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@implementation MM_Manager (MM_associated)
- (id)mm_getAssociatedObjectForKey:(const char *)key
{
const char * propName = key;
NSString * key_ = [NSString stringWithUTF8String:propName];
return MM_UserDefaults.mm_objectValue(key_);
}

- (id)mm_setAssignAssociatedObject:(id)obj forKey:(const char *)key;
{
const char * propName = key;
NSString * key_ = [NSString stringWithUTF8String:propName];
id oldValue = MM_UserDefaults.mm_objectValue(key_);
MM_UserDefaults.mm_addObject(key_ ,obj);
NSLog(@"userdefault:%@ for key:%@",obj,key_);
return oldValue;
}
@end

要了解如何使用宏,请参考👇的文章

为分类添加试用宏添加属性

对ReactiveCocoa中宏的解释

8.合理的修改readonly属性以及便捷的进行copy操作

一个场景是一个类通过一个参数初始化,然后他也有一个属性和这个参数一样,但是是readonly的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface Reminder : NSObject

@property (nonatomic, copy, readonly) NSString *title;
- (instancetype)initWith:(NSString *)title;

@impeletation

- (instancetype)initWithTitle:(NSString *)title
{
self = [super init];
if (self) {
_title = title;
}
return self;
}

然后就没有办法修改这个title属性了,虽然可以通过KVC,但是,想象一下有很多参数的时候,那就是比较麻烦的事情了。具体还有其他什么麻烦的情况,可以参考这个博文,里面也有应对麻烦场景下的解决办法,这些办法都不是很优雅,最后通过runtime特性做出了一个小巧的工具。

里面主要用到了runtime来获取类的所有属性、消息传递、KVC来对所有的属性进行赋值,主要涉及到两个方法

1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

- (void)forwardInvocation:(NSInvocation *)invocation;

具体NSInvocation的用法,一搜一大堆,这不是重点,根据以上两个方法可以将target对象进行KVC设置readonly属性,最后的工具方法有两个:

1
2
3
4
@interface NSObject (AHKBuilder)
- (instancetype)initWithBuilder_ahk:(void (^)(id))builderBlock;
- (instancetype)copyWithBuilder_ahk:(void (^)(id))builderBlock;
@end

有两种使用方法,第一种就是为readonly属性设置值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@protocol MM_testObjBuilder <NSObject>
@property (nonatomic ,retain ,readwrite) NSString * title;
@end

@interface MM_testObj : NSObject
@property (nonatomic ,copy) NSString * detail;
@property (nonatomic ,retain ,readonly) NSString * title;
- (instancetype) initWith:(NSString *)title;
@end

MM_testObj * obj = [[MM_testObj alloc] initWith:@"Name"];
obj.detail = @"Detail";
NSLog(@"%@/%@",obj.title,obj.detail);// Name/Detail

MM_testObj * obj_1 = [[MM_testObj alloc] initWithBuilder_ahk:^(id<MM_testObjBuilder>builder) {
builder.title = @"Name_1";
}];
NSLog(@"%@/%@",obj_1.title,obj_1.detail);// Name_1/(null)

第二种是对一个已有对象进行拷贝,说是拷贝,其实就是挨个对属性进行赋值,所以也就不需要被拷贝的类遵守NSCopying协议

1
2
3
4
5
6
7
8
9
MM_testObj * obj_2 = [obj copyWithBuilder_ahk:^(id<MM_testObjBuilder>builder) {

}];
NSLog(@"%@/%@",obj_2.title,obj_2.detail);// Name/Detail

MM_testObj * obj_3 = [obj copyWithBuilder_ahk:^(id<MM_testObjBuilder>builder) {
builder.title = @"Name_3";
}];
NSLog(@"%@/%@",obj_3.title,obj_3.detail);// Name_3/Detail

在正常开发中是不太会用到这种操作的,只不过是对Builder模式runtimeNSInvocation的一个小练习。当然,如果在逆向的时候需要hook一些类,那就是很实用的工具了。

参考文章

仓库地址


Continue update …